master
go 285 lines 7.83 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package vnoderegistry
4
5 import (
6 "fmt"
7 "sync"
8 "testing"
9
10 "github.com/netdata/netdata/go/plugins/pkg/netdataapi"
11 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/require"
13 )
14
15 func TestRegistryScenarios(t *testing.T) {
16 cases := map[string]struct {
17 run func(t *testing.T)
18 }{
19 "updates metadata and tracks owners": {
20 run: func(t *testing.T) {
21 reg := New()
22 first := netdataapi.HostInfo{
23 GUID: "node-guid",
24 Hostname: "node-a",
25 Labels: map[string]string{"_hostname": "node-a", "region": "eu"},
26 }
27 same := netdataapi.HostInfo{
28 GUID: "node-guid",
29 Hostname: "node-a",
30 Labels: map[string]string{"_hostname": "node-a", "region": "eu"},
31 }
32 conflict := netdataapi.HostInfo{
33 GUID: "node-guid",
34 Hostname: "node-b",
35 Labels: map[string]string{"_hostname": "node-b", "region": "us"},
36 }
37
38 got, err := reg.Register("job-a", first)
39 require.NoError(t, err)
40 assert.True(t, got.NeedDefine)
41 assert.True(t, got.OwnerAdded)
42 assert.False(t, got.MetadataUpdated)
43 assert.Equal(t, first, got.Info)
44
45 got, err = reg.Register("job-b", same)
46 require.NoError(t, err)
47 assert.False(t, got.NeedDefine)
48 assert.True(t, got.OwnerAdded)
49 assert.False(t, got.MetadataUpdated)
50 assert.Equal(t, first, got.Info)
51
52 got, err = reg.Register("job-c", conflict)
53 require.NoError(t, err)
54 assert.True(t, got.NeedDefine)
55 assert.True(t, got.OwnerAdded)
56 assert.True(t, got.MetadataUpdated)
57 assert.True(t, got.UpdateFirstSeen)
58 assert.Equal(t, conflict, got.Info)
59 assert.Equal(t, first, got.Previous)
60
61 got, err = reg.Register("job-c", conflict)
62 require.NoError(t, err)
63 assert.False(t, got.NeedDefine)
64 assert.False(t, got.OwnerAdded)
65 assert.False(t, got.MetadataUpdated)
66 assert.False(t, got.UpdateFirstSeen)
67
68 assert.Equal(t, []Owner{"job-a", "job-b", "job-c"}, reg.Owners("node-guid"))
69 assert.False(t, reg.Release("job-a", "node-guid"))
70 assert.Equal(t, 1, reg.Len())
71 assert.Equal(t, []Owner{"job-b", "job-c"}, reg.Owners("node-guid"))
72 assert.False(t, reg.Release("job-b", "node-guid"))
73 assert.True(t, reg.Release("job-c", "node-guid"))
74 assert.Equal(t, 0, reg.Len())
75 },
76 },
77 "normalizes metadata before compare": {
78 run: func(t *testing.T) {
79 reg := New()
80 first := netdataapi.HostInfo{
81 GUID: "node-guid",
82 Hostname: "node-a",
83 }
84 sameWireInfo := netdataapi.HostInfo{
85 GUID: " node-guid ",
86 Hostname: " node-a ",
87 Labels: map[string]string{
88 "_hostname": "node-a",
89 },
90 }
91
92 got, err := reg.Register("job-a", first)
93 require.NoError(t, err)
94 assert.True(t, got.NeedDefine)
95
96 got, err = reg.Register("job-b", sameWireInfo)
97 require.NoError(t, err)
98 assert.False(t, got.NeedDefine)
99 assert.Equal(t, map[string]string{"_hostname": "node-a"}, got.Info.Labels)
100 },
101 },
102 "rollback restores metadata update": {
103 run: func(t *testing.T) {
104 reg := New()
105 first := netdataapi.HostInfo{GUID: "node-guid", Hostname: "node-a"}
106 next := netdataapi.HostInfo{GUID: "node-guid", Hostname: "node-b"}
107
108 _, err := reg.Register("job-a", first)
109 require.NoError(t, err)
110 got, err := reg.Register("job-b", next)
111 require.NoError(t, err)
112 require.True(t, got.MetadataUpdated)
113 require.True(t, got.OwnerAdded)
114
115 reg.Rollback("job-b", got)
116
117 info, ok := reg.Lookup("node-guid")
118 require.True(t, ok)
119 assert.Equal(t, "node-a", info.Hostname)
120 assert.Equal(t, []Owner{"job-a"}, reg.Owners("node-guid"))
121 },
122 },
123 "rollback does not undo newer metadata update": {
124 run: func(t *testing.T) {
125 reg := New()
126 _, err := reg.Register("job-a", netdataapi.HostInfo{GUID: "node-guid", Hostname: "node-a"})
127 require.NoError(t, err)
128 rollback, err := reg.Register("job-b", netdataapi.HostInfo{GUID: "node-guid", Hostname: "node-b"})
129 require.NoError(t, err)
130 _, err = reg.Register("job-c", netdataapi.HostInfo{GUID: "node-guid", Hostname: "node-c"})
131 require.NoError(t, err)
132
133 reg.Rollback("job-b", rollback)
134
135 info, ok := reg.Lookup("node-guid")
136 require.True(t, ok)
137 assert.Equal(t, "node-c", info.Hostname)
138 assert.Equal(t, []Owner{"job-a", "job-c"}, reg.Owners("node-guid"))
139 },
140 },
141 "update warnings are per state and bounded": {
142 run: func(t *testing.T) {
143 reg := New()
144 _, err := reg.Register("job", netdataapi.HostInfo{GUID: "node-guid", Hostname: "node-a"})
145 require.NoError(t, err)
146
147 got, err := reg.Register("job", netdataapi.HostInfo{GUID: "node-guid", Hostname: "node-b"})
148 require.NoError(t, err)
149 assert.True(t, got.UpdateFirstSeen)
150
151 got, err = reg.Register("job", netdataapi.HostInfo{GUID: "node-guid", Hostname: "node-a"})
152 require.NoError(t, err)
153 assert.True(t, got.UpdateFirstSeen)
154
155 got, err = reg.Register("job", netdataapi.HostInfo{GUID: "node-guid", Hostname: "node-b"})
156 require.NoError(t, err)
157 assert.False(t, got.UpdateFirstSeen)
158
159 for i := range maxReportedMetadataStatesPerGUID + 1 {
160 _, err = reg.Register("job", netdataapi.HostInfo{
161 GUID: "node-guid",
162 Hostname: fmt.Sprintf("node-%d", i),
163 })
164 require.NoError(t, err)
165 }
166 reg.mu.Lock()
167 defer reg.mu.Unlock()
168 require.Len(t, reg.entries["node-guid"].reportedOrder, maxReportedMetadataStatesPerGUID)
169 require.Len(t, reg.entries["node-guid"].reportedStates, maxReportedMetadataStatesPerGUID)
170 },
171 },
172 }
173
174 for name, tc := range cases {
175 t.Run(name, tc.run)
176 }
177 }
178
179 func TestRegistryValidation(t *testing.T) {
180 cases := map[string]struct {
181 owner Owner
182 info netdataapi.HostInfo
183 wantErr string
184 }{
185 "missing owner": {
186 info: netdataapi.HostInfo{GUID: "guid", Hostname: "host"},
187 wantErr: "owner is required",
188 },
189 "missing guid": {
190 owner: "job",
191 info: netdataapi.HostInfo{Hostname: "host"},
192 wantErr: "host guid is required",
193 },
194 "missing hostname": {
195 owner: "job",
196 info: netdataapi.HostInfo{GUID: "guid"},
197 wantErr: "host hostname is required",
198 },
199 "unsafe hostname": {
200 owner: "job",
201 info: netdataapi.HostInfo{GUID: "guid", Hostname: "host\nname"},
202 wantErr: "unsupported characters",
203 },
204 }
205
206 for name, tc := range cases {
207 t.Run(name, func(t *testing.T) {
208 reg := New()
209 _, err := reg.Register(tc.owner, tc.info)
210 require.ErrorContains(t, err, tc.wantErr)
211 })
212 }
213 }
214
215 func TestRegistryConcurrentScenarios(t *testing.T) {
216 cases := map[string]struct {
217 run func(t *testing.T)
218 }{
219 "registration": {
220 run: func(t *testing.T) {
221 reg := New()
222 first := netdataapi.HostInfo{
223 GUID: "node-guid",
224 Hostname: "node",
225 }
226
227 const owners = 64
228 var wg sync.WaitGroup
229 errs := make(chan error, owners)
230 for i := range owners {
231 wg.Go(func() {
232 info := first
233 if i%2 == 1 {
234 info.Hostname = "node-conflict"
235 }
236 _, err := reg.Register(Owner(fmt.Sprintf("job-%d", i)), info)
237 errs <- err
238 })
239 }
240 wg.Wait()
241 close(errs)
242 for err := range errs {
243 require.NoError(t, err)
244 }
245
246 info, ok := reg.Lookup("node-guid")
247 require.True(t, ok)
248 require.NotEmpty(t, info.Hostname)
249 require.Len(t, reg.Owners("node-guid"), owners)
250 },
251 },
252 "register and release": {
253 run: func(t *testing.T) {
254 reg := New()
255 const owners = 64
256
257 var wg sync.WaitGroup
258 errs := make(chan error, owners)
259 for i := range owners {
260 wg.Go(func() {
261 owner := Owner(fmt.Sprintf("job-%d", i))
262 _, err := reg.Register(owner, netdataapi.HostInfo{
263 GUID: "node-guid",
264 Hostname: "node",
265 })
266 errs <- err
267 reg.Release(owner, "node-guid")
268 })
269 }
270 wg.Wait()
271 close(errs)
272 for err := range errs {
273 require.NoError(t, err)
274 }
275
276 assert.Empty(t, reg.Owners("node-guid"))
277 assert.Equal(t, 0, reg.Len())
278 },
279 },
280 }
281
282 for name, tc := range cases {
283 t.Run(name, tc.run)
284 }
285 }