master
go 315 lines 7.64 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 // Package vnoderegistry tracks v2 vnode HOST_DEFINE metadata shared across jobs.
4 package vnoderegistry
5
6 import (
7 "fmt"
8 "maps"
9 "slices"
10 "sort"
11 "strconv"
12 "strings"
13 "sync"
14
15 "github.com/netdata/netdata/go/plugins/pkg/netdataapi"
16 "github.com/netdata/netdata/go/plugins/plugin/framework/chartemit"
17 )
18
19 const maxReportedMetadataStatesPerGUID = 64
20
21 // Owner identifies one runtime owner of a vnode GUID.
22 //
23 // Jobruntime v2 uses stable per-job/per-scope owner IDs so a job can release
24 // registry ownership after it has emitted cleanup for the corresponding scope.
25 type Owner string
26
27 // Registration reports the result of registering vnode metadata for an owner.
28 type Registration struct {
29 // Info is the metadata retained by the registry after registration.
30 Info netdataapi.HostInfo
31
32 // Previous is set when an existing GUID's metadata was updated.
33 Previous netdataapi.HostInfo
34
35 // NeedDefine is true when this registration created or updated the registry entry.
36 NeedDefine bool
37
38 // OwnerAdded is true when this call added a new owner record.
39 OwnerAdded bool
40
41 // MetadataUpdated is true when Info replaced previously retained metadata.
42 MetadataUpdated bool
43
44 // UpdateFirstSeen is true only for the first occurrence of a distinct metadata
45 // transition. Callers should use this to avoid log spam.
46 UpdateFirstSeen bool
47
48 revision uint64
49 previousRevision uint64
50 }
51
52 type Registry struct {
53 mu sync.Mutex
54 entries map[string]*entry
55 }
56
57 type entry struct {
58 info netdataapi.HostInfo
59 revision uint64
60 owners map[Owner]struct{}
61 reportedStates map[string]struct{}
62 reportedOrder []string
63 }
64
65 // New returns an empty concurrency-safe vnode registry.
66 func New() *Registry {
67 return &Registry{entries: make(map[string]*entry)}
68 }
69
70 // Register records that owner emits metrics under info.GUID.
71 //
72 // New metadata for an existing GUID replaces the retained metadata. This keeps
73 // runtime vnode updates simple: callers should log MetadataUpdated as a warning
74 // because repeated conflicting writers can still cause metadata flip-flop.
75 func (r *Registry) Register(owner Owner, info netdataapi.HostInfo) (Registration, error) {
76 if r == nil {
77 return Registration{}, fmt.Errorf("vnoderegistry: nil registry")
78 }
79 owner = Owner(strings.TrimSpace(string(owner)))
80 if owner == "" {
81 return Registration{}, fmt.Errorf("vnoderegistry: owner is required")
82 }
83 info, err := chartemit.PrepareHostInfo(info)
84 if err != nil {
85 return Registration{}, fmt.Errorf("vnoderegistry: %w", err)
86 }
87
88 r.mu.Lock()
89 defer r.mu.Unlock()
90
91 if r.entries == nil {
92 r.entries = make(map[string]*entry)
93 }
94
95 ent, ok := r.entries[info.GUID]
96 if !ok {
97 ent = &entry{
98 info: cloneHostInfo(info),
99 revision: 1,
100 owners: map[Owner]struct{}{owner: {}},
101 reportedStates: make(map[string]struct{}),
102 }
103 r.entries[info.GUID] = ent
104 return Registration{
105 Info: cloneHostInfo(ent.info),
106 NeedDefine: true,
107 OwnerAdded: true,
108 revision: ent.revision,
109 }, nil
110 }
111
112 _, hadOwner := ent.owners[owner]
113 ent.owners[owner] = struct{}{}
114
115 result := Registration{
116 Info: cloneHostInfo(ent.info),
117 OwnerAdded: !hadOwner,
118 revision: ent.revision,
119 }
120 if hostInfoEqual(ent.info, info) {
121 return result, nil
122 }
123
124 previous := cloneHostInfo(ent.info)
125 previousRevision := ent.revision
126 ent.info = cloneHostInfo(info)
127 ent.revision++
128 result.Info = cloneHostInfo(ent.info)
129 result.Previous = previous
130 result.revision = ent.revision
131 result.previousRevision = previousRevision
132 result.NeedDefine = true
133 result.MetadataUpdated = true
134
135 updateKey := hostInfoFingerprint(info)
136 if markReportedState(ent, updateKey) {
137 result.UpdateFirstSeen = true
138 }
139 return result, nil
140 }
141
142 // Rollback undoes a registration that has not been emitted successfully.
143 //
144 // Rollback is best-effort: if another registration changed the same GUID after
145 // reg, the metadata restore is skipped to avoid undoing a later writer.
146 func (r *Registry) Rollback(owner Owner, reg Registration) {
147 if r == nil {
148 return
149 }
150 owner = Owner(strings.TrimSpace(string(owner)))
151 guid := strings.TrimSpace(reg.Info.GUID)
152 if owner == "" || guid == "" {
153 return
154 }
155
156 r.mu.Lock()
157 defer r.mu.Unlock()
158
159 ent, ok := r.entries[guid]
160 if !ok {
161 return
162 }
163 if reg.OwnerAdded {
164 delete(ent.owners, owner)
165 }
166 if reg.MetadataUpdated && ent.revision == reg.revision && hostInfoEqual(ent.info, reg.Info) {
167 ent.info = cloneHostInfo(reg.Previous)
168 ent.revision = reg.previousRevision
169 }
170 if len(ent.owners) == 0 {
171 delete(r.entries, guid)
172 }
173 }
174
175 // Release removes one owner record for guid. It returns true when the GUID entry
176 // was removed because no owners remain.
177 func (r *Registry) Release(owner Owner, guid string) bool {
178 if r == nil {
179 return false
180 }
181 owner = Owner(strings.TrimSpace(string(owner)))
182 guid = strings.TrimSpace(guid)
183 if owner == "" || guid == "" {
184 return false
185 }
186
187 r.mu.Lock()
188 defer r.mu.Unlock()
189
190 ent, ok := r.entries[guid]
191 if !ok {
192 return false
193 }
194 delete(ent.owners, owner)
195 if len(ent.owners) > 0 {
196 return false
197 }
198 delete(r.entries, guid)
199 return true
200 }
201
202 // Lookup returns the retained metadata for guid.
203 func (r *Registry) Lookup(guid string) (netdataapi.HostInfo, bool) {
204 if r == nil {
205 return netdataapi.HostInfo{}, false
206 }
207 guid = strings.TrimSpace(guid)
208 if guid == "" {
209 return netdataapi.HostInfo{}, false
210 }
211
212 r.mu.Lock()
213 defer r.mu.Unlock()
214
215 ent, ok := r.entries[guid]
216 if !ok {
217 return netdataapi.HostInfo{}, false
218 }
219 return cloneHostInfo(ent.info), true
220 }
221
222 // Owners returns the sorted owner IDs currently registered for guid.
223 func (r *Registry) Owners(guid string) []Owner {
224 if r == nil {
225 return nil
226 }
227 guid = strings.TrimSpace(guid)
228 if guid == "" {
229 return nil
230 }
231
232 r.mu.Lock()
233 defer r.mu.Unlock()
234
235 ent, ok := r.entries[guid]
236 if !ok {
237 return nil
238 }
239 owners := make([]Owner, 0, len(ent.owners))
240 for owner := range ent.owners {
241 owners = append(owners, owner)
242 }
243 slices.Sort(owners)
244 return owners
245 }
246
247 // Len returns the number of retained GUID entries.
248 func (r *Registry) Len() int {
249 if r == nil {
250 return 0
251 }
252 r.mu.Lock()
253 defer r.mu.Unlock()
254 return len(r.entries)
255 }
256
257 func cloneHostInfo(info netdataapi.HostInfo) netdataapi.HostInfo {
258 return netdataapi.HostInfo{
259 GUID: info.GUID,
260 Hostname: info.Hostname,
261 Labels: maps.Clone(info.Labels),
262 }
263 }
264
265 func hostInfoEqual(left, right netdataapi.HostInfo) bool {
266 return left.GUID == right.GUID &&
267 left.Hostname == right.Hostname &&
268 maps.Equal(left.Labels, right.Labels)
269 }
270
271 func hostInfoFingerprint(info netdataapi.HostInfo) string {
272 var b strings.Builder
273 writeHostInfoFingerprint(&b, info)
274 return b.String()
275 }
276
277 func writeHostInfoFingerprint(b *strings.Builder, info netdataapi.HostInfo) {
278 writeFingerprintPart(b, info.GUID)
279 writeFingerprintPart(b, info.Hostname)
280
281 keys := make([]string, 0, len(info.Labels))
282 for key := range info.Labels {
283 keys = append(keys, key)
284 }
285 sort.Strings(keys)
286 for _, key := range keys {
287 writeFingerprintPart(b, key)
288 writeFingerprintPart(b, info.Labels[key])
289 }
290 }
291
292 func writeFingerprintPart(b *strings.Builder, value string) {
293 b.WriteString(strconv.Itoa(len(value)))
294 b.WriteByte(':')
295 b.WriteString(value)
296 b.WriteByte('\xff')
297 }
298
299 func markReportedState(ent *entry, key string) bool {
300 if ent.reportedStates == nil {
301 ent.reportedStates = make(map[string]struct{})
302 }
303 if _, seen := ent.reportedStates[key]; seen {
304 return false
305 }
306 if len(ent.reportedOrder) >= maxReportedMetadataStatesPerGUID {
307 evicted := ent.reportedOrder[0]
308 copy(ent.reportedOrder, ent.reportedOrder[1:])
309 ent.reportedOrder = ent.reportedOrder[:len(ent.reportedOrder)-1]
310 delete(ent.reportedStates, evicted)
311 }
312 ent.reportedStates[key] = struct{}{}
313 ent.reportedOrder = append(ent.reportedOrder, key)
314 return true
315 }