master
go 829 lines 21.5 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package jobruntime
4
5 import (
6 "bytes"
7 "context"
8 "errors"
9 "fmt"
10 "io"
11 "log/slog"
12 "maps"
13 "runtime/debug"
14 "sync"
15 "sync/atomic"
16 "time"
17
18 "github.com/netdata/netdata/go/plugins/logger"
19 "github.com/netdata/netdata/go/plugins/pkg/metrix"
20 "github.com/netdata/netdata/go/plugins/pkg/netdataapi"
21 "github.com/netdata/netdata/go/plugins/plugin/framework/chartemit"
22 "github.com/netdata/netdata/go/plugins/plugin/framework/chartengine"
23 "github.com/netdata/netdata/go/plugins/plugin/framework/collectorapi"
24 "github.com/netdata/netdata/go/plugins/plugin/framework/runtimecomp"
25 "github.com/netdata/netdata/go/plugins/plugin/framework/tickstate"
26 "github.com/netdata/netdata/go/plugins/plugin/framework/vnoderegistry"
27 "github.com/netdata/netdata/go/plugins/plugin/framework/vnodes"
28 )
29
30 type JobV2Config struct {
31 PluginName string
32 Name string
33 ModuleName string
34 FullName string
35 Source string
36 Module collectorapi.CollectorV2
37 Labels map[string]string
38 Out io.Writer
39 UpdateEvery int
40 AutoDetectEvery int
41 IsStock bool
42 Vnode vnodes.VirtualNode
43 VnodeRegistry *vnoderegistry.Registry
44 FunctionOnly bool
45 RuntimeService runtimecomp.Service
46 }
47
48 func NewJobV2(cfg JobV2Config) *JobV2 {
49 var buf bytes.Buffer
50 if cfg.UpdateEvery <= 0 {
51 cfg.UpdateEvery = 1
52 }
53 registry := cfg.VnodeRegistry
54 if registry == nil {
55 registry = vnoderegistry.New()
56 }
57
58 j := &JobV2{
59 pluginName: cfg.PluginName,
60 name: cfg.Name,
61 moduleName: cfg.ModuleName,
62 fullName: cfg.FullName,
63 updateEvery: cfg.UpdateEvery,
64 autoDetectEvery: cfg.AutoDetectEvery,
65 autoDetectTries: infTries,
66 isStock: cfg.IsStock,
67 functionOnly: cfg.FunctionOnly,
68 module: cfg.Module,
69 labels: cloneLabels(cfg.Labels),
70 out: cfg.Out,
71 stopCtrl: newStopController(),
72 tick: make(chan int),
73 updVnode: make(chan *vnodes.VirtualNode, 1),
74 buf: &buf,
75 api: netdataapi.New(&buf),
76 vnode: cfg.Vnode,
77 vnodeRegistry: registry,
78 runtimeService: cfg.RuntimeService,
79 }
80 if j.out == nil {
81 j.out = io.Discard
82 }
83
84 log := logger.New().With(jobLoggerAttrs(j.ModuleName(), j.Name(), cfg.Source)...)
85 j.Logger = log
86 if j.module != nil {
87 j.module.GetBase().Logger = log
88 if vnode := j.module.VirtualNode(); vnode != nil {
89 *vnode = *cfg.Vnode.Copy()
90 }
91 }
92 return j
93 }
94
95 type JobV2 struct {
96 pluginName string
97 name string
98 moduleName string
99 fullName string
100 updateEvery int
101 autoDetectEvery int
102 autoDetectTries int
103 isStock bool
104 functionOnly bool
105 labels map[string]string
106
107 *logger.Logger
108
109 module collectorapi.CollectorV2
110
111 running atomic.Bool
112
113 initialized bool
114 panicked atomic.Bool
115
116 store metrix.CollectorStore
117 cycle metrix.CycleController
118
119 scopeStates map[string]*jobV2ScopeState
120 chartTemplateYAML []byte
121 chartTemplateRevision uint64
122 engineOptions []chartengine.Option
123 runtimeStore metrix.RuntimeStore
124 runtimeAggregator *chartengine.RuntimeAggregator
125
126 prevRun time.Time
127 retries atomic.Int64
128
129 vnodeMu sync.RWMutex
130 vnode vnodes.VirtualNode
131 updVnode chan *vnodes.VirtualNode
132
133 vnodeRegistry *vnoderegistry.Registry
134
135 ctxMu sync.RWMutex
136 runCtx context.Context
137 cancelRun context.CancelFunc
138
139 tick chan int
140 out io.Writer
141 buf *bytes.Buffer
142 api *netdataapi.API
143
144 stopCtrl stopController
145
146 runtimeService runtimecomp.Service
147 runtimeComponentName string
148 runtimeComponentRegistered bool
149
150 skipTracker tickstate.SkipTracker
151 }
152
153 type jobV2PreparedEmission struct {
154 scopes []jobV2PreparedScopeEmission
155 scopeFailure bool
156 }
157
158 type jobV2PreparedScopeEmission struct {
159 scope *jobV2ScopeState
160 attempt chartengine.PlanAttempt
161 plan chartengine.Plan
162 decision jobV2EmissionDecision
163 output []byte
164 live bool
165 }
166
167 type jobV2ScopeState struct {
168 scopeKey string
169 scope metrix.HostScope
170 engine *chartengine.Engine
171 host jobV2HostState
172 }
173
174 func (j *JobV2) FullName() string { return j.fullName }
175 func (j *JobV2) ModuleName() string { return j.moduleName }
176 func (j *JobV2) Name() string { return j.name }
177 func (j *JobV2) Panicked() bool { return j.panicked.Load() }
178 func (j *JobV2) IsRunning() bool { return j.running.Load() }
179 func (j *JobV2) Module() collectorapi.CollectorV2 { return j.module }
180 func (j *JobV2) Collector() any { return j.module }
181 func (j *JobV2) AutoDetectionEvery() int {
182 return j.autoDetectEvery
183 }
184 func (j *JobV2) RetryAutoDetection() bool {
185 return retryAutoDetection(j.autoDetectEvery, j.autoDetectTries)
186 }
187 func (j *JobV2) Configuration() any {
188 if j.module == nil {
189 return nil
190 }
191 return j.module.Configuration()
192 }
193 func (j *JobV2) IsFunctionOnly() bool { return j.functionOnly }
194 func (j *JobV2) Vnode() vnodes.VirtualNode {
195 j.vnodeMu.RLock()
196 defer j.vnodeMu.RUnlock()
197 return *j.vnode.Copy()
198 }
199 func (j *JobV2) UpdateVnode(vnode *vnodes.VirtualNode) {
200 if vnode == nil {
201 return
202 }
203 select {
204 case <-j.updVnode:
205 default:
206 }
207 j.updVnode <- vnode
208 }
209 func (j *JobV2) Cleanup() {
210 j.buf.Reset()
211 snapshots := j.captureScopeCleanupSnapshots()
212 j.unregisterRuntimeComponent()
213 if j.module != nil {
214 j.module.Cleanup(context.Background())
215 }
216 if !collectorapi.ShouldObsoleteCharts() {
217 j.releaseAllScopeRegistryOwners()
218 j.clearAllScopeStateAfterCleanup()
219 return
220 }
221
222 for _, snapshot := range snapshots {
223 if snapshot.staleVnodeSuppressed || len(snapshot.charts) == 0 {
224 continue
225 }
226
227 env := chartemit.EmitEnv{
228 TypeID: j.fullName,
229 UpdateEvery: j.updateEvery,
230 Plugin: j.pluginName,
231 Module: j.moduleName,
232 JobName: j.name,
233 JobLabels: j.labels,
234 }
235 if snapshot.host.isVnode() {
236 env.HostScope = &chartemit.HostScope{GUID: snapshot.host.guid}
237 }
238 j.buf.Reset()
239 if err := chartemit.ApplyPlan(j.api, buildJobV2CleanupPlan(snapshot.charts), env); err != nil {
240 j.Warningf("cleanup apply plan failed for host scope %q: %v", snapshot.scopeKey, err)
241 j.buf.Reset()
242 continue
243 }
244 _, _ = io.Copy(j.out, j.buf)
245 j.buf.Reset()
246 }
247 j.releaseAllScopeRegistryOwners()
248 j.clearAllScopeStateAfterCleanup()
249 }
250
251 func (j *JobV2) AutoDetection() (err error) {
252 defer func() {
253 if r := recover(); r != nil {
254 err = fmt.Errorf("panic %v", r)
255 j.panicked.Store(true)
256 j.disableAutoDetection()
257 j.Errorf("PANIC %v", r)
258 if logger.Level.Enabled(slog.LevelDebug) {
259 j.Errorf("STACK: %s", debug.Stack())
260 }
261 }
262 if err != nil {
263 j.Cleanup()
264 }
265 }()
266 if j.isStock {
267 j.Mute()
268 }
269
270 if err = j.init(); err != nil {
271 j.Errorf("init failed: %v", err)
272 j.Unmute()
273 j.disableAutoDetection()
274 return err
275 }
276 if err = j.check(); err != nil {
277 j.Errorf("check failed: %v", err)
278 j.Unmute()
279 return err
280 }
281 j.Unmute()
282 j.Info("check success")
283 if err = j.postCheck(); err != nil {
284 j.Errorf("postCheck failed: %v", err)
285 j.disableAutoDetection()
286 return err
287 }
288 return nil
289 }
290
291 func (j *JobV2) Start() {
292 j.stopCtrl.markStarted()
293 j.running.Store(true)
294 runCtx, cancel := context.WithCancel(context.Background())
295 j.setRunContext(runCtx, cancel)
296 if j.functionOnly {
297 j.Info("started in function-only mode")
298 } else {
299 j.Infof("started (v2), data collection interval %ds", j.updateEvery)
300 }
301 defer func() {
302 cancel()
303 j.setRunContext(nil, nil)
304 j.stopCtrl.markStopped()
305 j.Info("stopped")
306 }()
307
308 LOOP:
309 for {
310 select {
311 case <-j.stopCtrl.stopCh:
312 break LOOP
313 case t := <-j.tick:
314 if !j.functionOnly && j.shouldCollect(t) {
315 markRunStartWithResumeLog(&j.skipTracker, j.Logger)
316 j.runOnce()
317 j.skipTracker.MarkRunStop(time.Now())
318 }
319 }
320 }
321 // Mark not-running before cleanup so external function dispatch can reject requests
322 // while module resources are being torn down.
323 j.running.Store(false)
324 j.Cleanup()
325 }
326
327 func (j *JobV2) Stop() {
328 j.cancelRunContext()
329 j.stopCtrl.stopAndWait()
330 }
331
332 func (j *JobV2) Tick(clock int) {
333 enqueueTickWithSkipLog(j.tick, clock, j.functionOnly, j.updateEvery, int(j.retries.Load()), &j.skipTracker, j.Logger)
334 }
335
336 func (j *JobV2) shouldCollect(clock int) bool {
337 return shouldCollectWithPenalty(clock, j.updateEvery, int(j.retries.Load()))
338 }
339
340 func (j *JobV2) init() error {
341 if j.initialized {
342 return nil
343 }
344 if err := j.module.Init(j.moduleContext()); err != nil {
345 return err
346 }
347 j.initialized = true
348 return nil
349 }
350
351 func (j *JobV2) check() error {
352 if err := j.module.Check(j.moduleContext()); err != nil {
353 consumeAutoDetectTry(&j.autoDetectTries)
354 return err
355 }
356 return nil
357 }
358
359 func (j *JobV2) postCheck() error {
360 if j.functionOnly {
361 // Match v1 semantics: function-only jobs validate connectivity only.
362 return nil
363 }
364
365 store := j.module.MetricStore()
366 if store == nil {
367 return fmt.Errorf("nil metric store")
368 }
369 managed, ok := metrix.AsCycleManagedStore(store)
370 if !ok {
371 return fmt.Errorf("metric store is not cycle-managed")
372 }
373
374 opts := []chartengine.Option{
375 chartengine.WithLogger(j.Logger.With(slog.String("component", "chartengine"))),
376 chartengine.WithEmitTypeIDBudgetPrefix(j.fullName),
377 }
378 if v, ok := j.module.(collectorapi.CollectorV2EnginePolicy); ok {
379 opts = append(opts, chartengine.WithEnginePolicy(v.EnginePolicy()))
380 }
381
382 templateYAML := []byte(j.module.ChartTemplateYAML())
383 if err := validateJobV2ChartTemplate(templateYAML, opts); err != nil {
384 return err
385 }
386
387 j.store = store
388 j.cycle = managed.CycleController()
389 j.scopeStates = make(map[string]*jobV2ScopeState)
390 j.chartTemplateYAML = templateYAML
391 j.chartTemplateRevision = 1
392 j.engineOptions = opts
393 j.runtimeStore = metrix.NewRuntimeStore()
394 j.runtimeAggregator = chartengine.NewRuntimeAggregator(j.runtimeStore)
395 if err := j.registerRuntimeComponent(); err != nil {
396 j.Warningf("runtime metrics registration failed: %v", err)
397 }
398 return nil
399 }
400
401 func validateJobV2ChartTemplate(templateYAML []byte, opts []chartengine.Option) error {
402 engineOpts := append([]chartengine.Option{}, opts...)
403 engineOpts = append(engineOpts, chartengine.WithRuntimeStore(nil))
404 engine, err := chartengine.New(engineOpts...)
405 if err != nil {
406 return err
407 }
408 return engine.LoadYAML(templateYAML, 1)
409 }
410
411 func (j *JobV2) runOnce() {
412 defer j.ResetAllOnce()
413 defer j.flushRuntimeAggregator()
414
415 j.applyPendingVnodeUpdate()
416
417 curTime := time.Now()
418 sinceLastRun := calcSinceLastRun(curTime, j.prevRun)
419 j.prevRun = curTime
420
421 prepared, ok := j.collectAndEmit(sinceLastRun)
422 if ok && !j.panicked.Load() {
423 if err := j.finishPreparedEmission(prepared); err != nil {
424 j.Warningf("finalize emission failed: %v", err)
425 ok = false
426 }
427 }
428 if ok {
429 j.retries.Store(0)
430 } else {
431 j.retries.Add(1)
432 }
433 j.buf.Reset()
434 }
435
436 func (j *JobV2) flushRuntimeAggregator() {
437 if j != nil && j.runtimeAggregator != nil {
438 j.runtimeAggregator.Flush()
439 }
440 }
441
442 func (j *JobV2) applyPendingVnodeUpdate() {
443 select {
444 case vnode := <-j.updVnode:
445 if vnode == nil {
446 return
447 }
448 if j.module != nil && j.module.VirtualNode() != nil {
449 // Match v1 ownership model: do not override module-owned vnode state.
450 j.Debugf("ignoring vnode update for module-owned vnode")
451 return
452 }
453
454 next := vnode.Copy()
455
456 j.vnodeMu.Lock()
457 j.vnode = *next
458 j.vnodeMu.Unlock()
459 // Registry owner release is intentionally tied to the next successful
460 // emission or cleanup, so obsolete emission can still select the old host.
461 if state := j.scopeStates[defaultHostScopeKey]; state != nil {
462 state.host.invalidateDefine()
463 }
464 default:
465 }
466 }
467
468 func (j *JobV2) collectAndEmit(sinceLastRun int) (prepared jobV2PreparedEmission, ok bool) {
469 j.panicked.Store(false)
470 cycleOpen := false
471
472 defer func() {
473 if r := recover(); r != nil {
474 j.rollbackPreparedEmission(prepared)
475 j.buf.Reset()
476 if j.runtimeAggregator != nil {
477 j.runtimeAggregator.Reset()
478 }
479 if cycleOpen {
480 // Recover path must close staged frame to keep subsequent cycles valid.
481 func() {
482 defer func() { _ = recover() }()
483 j.cycle.AbortCycle()
484 }()
485 }
486 j.abortPreparedEmission(prepared)
487 j.panicked.Store(true)
488 j.Errorf("PANIC: %v", r)
489 if logger.Level.Enabled(slog.LevelDebug) {
490 j.Errorf("STACK: %s", debug.Stack())
491 }
492 }
493 }()
494
495 j.cycle.BeginCycle()
496 cycleOpen = true
497 if err := j.module.Collect(j.moduleContext()); err != nil {
498 j.cycle.AbortCycle()
499 cycleOpen = false
500 j.Warningf("collect failed: %v", err)
501 return jobV2PreparedEmission{}, false
502 }
503 if err := j.cycle.CommitCycleSuccess(); err != nil {
504 cycleOpen = false
505 j.Warningf("commit cycle failed: %v", err)
506 return jobV2PreparedEmission{}, false
507 }
508 cycleOpen = false
509
510 liveSet := j.liveScopeSet()
511 workSet := j.scopeWorkSet(liveSet)
512 for _, scopeKey := range sortedScopeKeys(workSet) {
513 scope := workSet[scopeKey]
514 _, live := liveSet[scopeKey]
515 if !live {
516 if state := j.scopeStates[scopeKey]; state != nil {
517 scope = state.scope
518 }
519 }
520 scopePrepared, scopeOK := j.prepareScopeEmission(scope, live, sinceLastRun)
521 if !scopeOK {
522 prepared.scopeFailure = true
523 continue
524 }
525 prepared.scopes = append(prepared.scopes, scopePrepared)
526 }
527 j.Debugf("v2 scope count: %d", len(j.scopeStates))
528 if len(prepared.scopes) == 0 && prepared.scopeFailure {
529 return prepared, false
530 }
531 return prepared, true
532 }
533
534 func (j *JobV2) finishPreparedEmission(prepared jobV2PreparedEmission) error {
535 successes := 0
536 failures := 0
537 var finalErr error
538 for _, scope := range prepared.scopes {
539 if err := scope.attempt.Commit(); err != nil {
540 j.rollbackVnodeRegistryEmission(scope.decision)
541 failures++
542 finalErr = errors.Join(finalErr, err)
543 j.Warningf("finalize emission for host scope %q failed: %v", scope.scope.scopeKey, err)
544 continue
545 }
546 if len(scope.output) > 0 {
547 _, _ = j.out.Write(scope.output)
548 }
549 j.commitScopeEmission(scope)
550 successes++
551 }
552 if prepared.scopeFailure {
553 failures++
554 }
555 if successes == 0 && failures > 0 {
556 if finalErr != nil {
557 return finalErr
558 }
559 return fmt.Errorf("all host scope emissions failed")
560 }
561 return nil
562 }
563
564 func (j *JobV2) prepareScopeEmission(scope metrix.HostScope, live bool, sinceLastRun int) (prepared jobV2PreparedScopeEmission, ok bool) {
565 var attempt chartengine.PlanAttempt
566 var decision jobV2EmissionDecision
567 defer func() {
568 if r := recover(); r != nil {
569 j.rollbackVnodeRegistryEmission(decision)
570 attempt.Abort()
571 j.buf.Reset()
572 panic(r)
573 }
574 if !ok {
575 j.rollbackVnodeRegistryEmission(decision)
576 attempt.Abort()
577 j.buf.Reset()
578 }
579 }()
580
581 state, err := j.ensureScopeState(scope)
582 if err != nil {
583 j.Warningf("prepare host scope %q failed: %v", scope.ScopeKey, err)
584 return jobV2PreparedScopeEmission{}, false
585 }
586
587 if state.scopeKey == defaultHostScopeKey {
588 vnode := j.currentVnode()
589 decision, err = state.host.prepareEmission(vnode)
590 if err == nil && decision.needEngineReload {
591 state.engine.ResetMaterialized()
592 }
593 if err != nil {
594 j.Warningf("prepare default host scope failed: %v", err)
595 return jobV2PreparedScopeEmission{}, false
596 }
597 } else {
598 decision, err = state.host.prepareScopedEmission(state.scope)
599 if err == nil && decision.needEngineReload {
600 state.engine.ResetMaterialized()
601 }
602 if err != nil {
603 j.Warningf("prepare host scope %q failed: %v", state.scopeKey, err)
604 return jobV2PreparedScopeEmission{}, false
605 }
606 }
607
608 attempt, err = state.engine.PreparePlan(j.store.Read(metrix.ReadRaw(), metrix.ReadFlatten(), metrix.ReadHostScope(state.scopeKey)))
609 if err != nil {
610 j.Warningf("build plan for host scope %q failed: %v", state.scopeKey, err)
611 return jobV2PreparedScopeEmission{}, false
612 }
613 plan := attempt.Plan()
614 if err := j.prepareScopeVnodeRegistryEmission(state, &decision, plan); err != nil {
615 j.Warningf("prepare vnode registry for host scope %q failed: %v", state.scopeKey, err)
616 return jobV2PreparedScopeEmission{}, false
617 }
618
619 j.buf.Reset()
620 env := j.emitEnv(sinceLastRun, decision)
621 if err := chartemit.ApplyPlan(j.api, plan, env); err != nil {
622 j.Warningf("apply plan for host scope %q failed: %v", state.scopeKey, err)
623 return jobV2PreparedScopeEmission{}, false
624 }
625 output := append([]byte(nil), j.buf.Bytes()...)
626 j.buf.Reset()
627
628 prepared = jobV2PreparedScopeEmission{
629 scope: state,
630 attempt: attempt,
631 plan: plan,
632 decision: decision,
633 output: output,
634 live: live,
635 }
636 return prepared, true
637 }
638
639 func (j *JobV2) commitScopeEmission(prepared jobV2PreparedScopeEmission) {
640 if prepared.scope == nil {
641 return
642 }
643 state := prepared.scope
644 if state.scopeKey == defaultHostScopeKey || prepared.decision.registryOwner != "" {
645 keep := make(map[vnoderegistry.Owner]struct{}, 1)
646 if prepared.decision.registryOwner != "" {
647 keep[prepared.decision.registryOwner] = struct{}{}
648 }
649 state.host.releaseSupersededRegistryOwnersExcept(
650 j.vnodeRegistry,
651 keep,
652 j.vnodeRegistryOwnerNamespacePrefix(state.scopeKey),
653 )
654 }
655 state.host.commitSuccessfulEmission(prepared.plan, prepared.decision)
656 if !prepared.live && len(state.host.cleanupCharts) == 0 {
657 state.host.releaseRegistryOwners(j.vnodeRegistry)
658 delete(j.scopeStates, state.scopeKey)
659 }
660 }
661
662 func (j *JobV2) rollbackPreparedEmission(prepared jobV2PreparedEmission) {
663 for _, scope := range prepared.scopes {
664 j.rollbackVnodeRegistryEmission(scope.decision)
665 }
666 }
667
668 func (j *JobV2) abortPreparedEmission(prepared jobV2PreparedEmission) {
669 for _, scope := range prepared.scopes {
670 scope.attempt.Abort()
671 }
672 }
673
674 func (j *JobV2) emitEnv(sinceLastRun int, decision jobV2EmissionDecision) chartemit.EmitEnv {
675 env := chartemit.EmitEnv{
676 TypeID: j.fullName,
677 UpdateEvery: j.updateEvery,
678 Plugin: j.pluginName,
679 Module: j.moduleName,
680 JobName: j.name,
681 JobLabels: j.labels,
682 MSSinceLast: sinceLastRun,
683 }
684 env.HostScope = decision.hostScope
685 return env
686 }
687
688 func (j *JobV2) currentVnode() vnodes.VirtualNode {
689 if j.module != nil {
690 if vnode := j.module.VirtualNode(); vnode != nil {
691 return *vnode.Copy()
692 }
693 }
694 j.vnodeMu.RLock()
695 defer j.vnodeMu.RUnlock()
696 return *j.vnode.Copy()
697 }
698
699 func (j *JobV2) prepareScopeVnodeRegistryEmission(state *jobV2ScopeState, decision *jobV2EmissionDecision, plan chartengine.Plan) error {
700 if decision == nil || !decision.targetHost.isVnode() || len(plan.Actions) == 0 {
701 return nil
702 }
703 if state == nil {
704 return fmt.Errorf("nil host scope state")
705 }
706 if state.scopeKey == defaultHostScopeKey {
707 vnode := j.currentVnode()
708 return j.prepareVnodeRegistryEmission(decision, j.vnodeRegistryOwner(decision.targetHost), netdataapi.HostInfo{
709 GUID: vnode.GUID,
710 Hostname: vnode.Hostname,
711 Labels: vnode.Labels,
712 })
713 }
714 return j.prepareVnodeRegistryEmission(decision, j.vnodeRegistryScopedOwner(state.scopeKey, state.scope.GUID), metrixHostScopeInfo(state.scope))
715 }
716
717 func (j *JobV2) prepareVnodeRegistryEmission(decision *jobV2EmissionDecision, owner vnoderegistry.Owner, info netdataapi.HostInfo) error {
718 registryInfo := netdataapi.HostInfo{
719 GUID: info.GUID,
720 Hostname: info.Hostname,
721 Labels: maps.Clone(info.Labels),
722 }
723 result, err := j.vnodeRegistry.Register(owner, registryInfo)
724 if err != nil {
725 return err
726 }
727 if result.MetadataUpdated && result.UpdateFirstSeen {
728 j.Warningf(
729 "vnode registry metadata updated for guid %q: hostname %q replaced by %q",
730 result.Info.GUID,
731 result.Previous.Hostname,
732 result.Info.Hostname,
733 )
734 }
735
736 scope := &chartemit.HostScope{GUID: decision.targetHost.guid}
737 if result.NeedDefine {
738 scope.Define = &result.Info
739 }
740 decision.hostScope = scope
741 decision.defineInfo = result.Info
742 decision.registryOwner = owner
743 decision.registryRegistration = result
744 return nil
745 }
746
747 func (j *JobV2) rollbackVnodeRegistryEmission(decision jobV2EmissionDecision) {
748 if decision.registryOwner != "" {
749 j.vnodeRegistry.Rollback(decision.registryOwner, decision.registryRegistration)
750 }
751 }
752
753 const vnodeRegistryOwnerSeparator = "\xff"
754
755 func (j *JobV2) vnodeRegistryOwnerPrefix() string {
756 // Keep the separator outside valid metrix scope keys and GUIDs so owner
757 // strings remain unambiguous without allocating a structured key.
758 return j.fullName + vnodeRegistryOwnerSeparator
759 }
760
761 func (j *JobV2) vnodeRegistryJobOwnerPrefix() string {
762 // Keep job-level vnode owners separate from future per-scope owners.
763 return j.vnodeRegistryOwnerPrefix() + "job" + vnodeRegistryOwnerSeparator
764 }
765
766 func (j *JobV2) vnodeRegistryScopedOwnerPrefix(scopeKey string) string {
767 return j.vnodeRegistryOwnerPrefix() + "scope" + vnodeRegistryOwnerSeparator + scopeKey + vnodeRegistryOwnerSeparator
768 }
769
770 func (j *JobV2) vnodeRegistryOwnerNamespacePrefix(scopeKey string) string {
771 if scopeKey == defaultHostScopeKey {
772 return j.vnodeRegistryJobOwnerPrefix()
773 }
774 return j.vnodeRegistryScopedOwnerPrefix(scopeKey)
775 }
776
777 func (j *JobV2) vnodeRegistryOwner(target jobV2HostRef) vnoderegistry.Owner {
778 return vnoderegistry.Owner(j.vnodeRegistryJobOwnerPrefix() + target.guid)
779 }
780
781 func (j *JobV2) vnodeRegistryScopedOwner(scopeKey, guid string) vnoderegistry.Owner {
782 return vnoderegistry.Owner(j.vnodeRegistryScopedOwnerPrefix(scopeKey) + guid)
783 }
784
785 func (j *JobV2) penalty() int {
786 return penaltyFromRetries(int(j.retries.Load()), j.updateEvery)
787 }
788
789 func (j *JobV2) disableAutoDetection() {
790 disableAutoDetection(&j.autoDetectEvery)
791 }
792
793 func cloneLabels(in map[string]string) map[string]string {
794 if len(in) == 0 {
795 return nil
796 }
797 out := make(map[string]string, len(in))
798 maps.Copy(out, in)
799 return out
800 }
801
802 func (j *JobV2) moduleContext() context.Context {
803 j.ctxMu.RLock()
804 ctx := j.runCtx
805 j.ctxMu.RUnlock()
806 if ctx == nil {
807 ctx = context.Background()
808 }
809 if j.runtimeService != nil {
810 return runtimecomp.ContextWithService(ctx, j.runtimeService)
811 }
812 return ctx
813 }
814
815 func (j *JobV2) cancelRunContext() {
816 j.ctxMu.RLock()
817 cancel := j.cancelRun
818 j.ctxMu.RUnlock()
819 if cancel != nil {
820 cancel()
821 }
822 }
823
824 func (j *JobV2) setRunContext(ctx context.Context, cancel context.CancelFunc) {
825 j.ctxMu.Lock()
826 j.runCtx = ctx
827 j.cancelRun = cancel
828 j.ctxMu.Unlock()
829 }