master
go 686 lines 20.5 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package chartengine
4
5 import (
6 "fmt"
7 "math"
8 "sort"
9 "strings"
10 "time"
11
12 "github.com/netdata/netdata/go/plugins/pkg/metrix"
13 "github.com/netdata/netdata/go/plugins/plugin/framework/chartengine/internal/program"
14 )
15
16 type labelSliceView struct {
17 items []metrix.Label
18 }
19
20 func (v labelSliceView) Get(key string) (string, bool) {
21 for _, item := range v.items {
22 if item.Key == key {
23 return item.Value, true
24 }
25 if item.Key > key {
26 break
27 }
28 }
29 return "", false
30 }
31
32 func (v labelSliceView) Range(fn func(key, value string) bool) {
33 for _, item := range v.items {
34 if !fn(item.Key, item.Value) {
35 return
36 }
37 }
38 }
39
40 func (v labelSliceView) Len() int {
41 return len(v.items)
42 }
43
44 func (v labelSliceView) CloneMap() map[string]string {
45 out := make(map[string]string, len(v.items))
46 for _, item := range v.items {
47 out[item.Key] = item.Value
48 }
49 return out
50 }
51
52 // Plan is the deterministic planner output consumed by chartemit.
53 //
54 // Current scope:
55 // - create/update/remove actions for chart and dimension lifecycle,
56 // - inferred dynamic dimension names resolved from flattened metric metadata.
57 type Plan struct {
58 Actions []EngineAction
59 InferredDimensions []InferredDimension
60 }
61
62 // InferredDimension is one resolved dynamic dimension name from planner input.
63 type InferredDimension struct {
64 ChartTemplateID string
65 DimensionIndex int
66 Name string
67 }
68
69 type dimensionState struct {
70 hidden bool
71 float bool
72 static bool
73 order int
74 algorithm program.Algorithm
75 multiplier int
76 divisor int
77 }
78
79 type dimBuildEntry struct {
80 seenSeq uint64
81 value metrix.SampleValue
82 dimensionState
83 }
84
85 type chartState struct {
86 templateID string
87 chartID string
88 meta program.ChartMeta
89 lifecycle program.LifecyclePolicy
90 labels *chartLabelAccumulator
91 entries map[string]*dimBuildEntry
92 observedCount int
93 currentBuildSeq uint64
94 }
95
96 type planBuildContext struct {
97 out *Plan
98 reader metrix.Reader
99 collectMeta metrix.CollectMeta
100 buildCycle uint64
101 prog *program.Program
102 cache *routeCache
103 index matchIndex
104 flat metrix.Reader
105
106 seenInfer map[string]struct{}
107 chartsByID map[string]*chartState
108 chartOwners map[string]string
109 dimCapHints map[string]int
110 materialized *materializedState
111 materializedByID map[string]*materializedChartState
112
113 planRouteStats
114 }
115
116 type flattenedReadChecker interface {
117 FlattenedRead() bool
118 }
119
120 func (e *Engine) preparePlan(reader metrix.Reader) (Plan, materializedState, uint64, uint64, uint64, bool, error) {
121 if e == nil {
122 return Plan{}, materializedState{}, 0, 0, 0, false, fmt.Errorf("chartengine: nil engine")
123 }
124 if reader == nil {
125 return Plan{}, materializedState{}, 0, 0, 0, false, fmt.Errorf("chartengine: nil metrics reader")
126 }
127 out := Plan{
128 Actions: make([]EngineAction, 0),
129 InferredDimensions: make([]InferredDimension, 0),
130 }
131 collectMeta := reader.CollectMeta()
132
133 e.mu.Lock()
134 defer e.mu.Unlock()
135 if e.state.outstanding != 0 {
136 return Plan{}, materializedState{}, 0, 0, 0, false, ErrOutstandingPlanAttempt
137 }
138 sample := PlanRuntimeSample{startedAt: time.Now()}
139 defer func() { e.observeBuildSample(sample) }()
140 // Failed attempt must not trigger lifecycle transitions.
141 if collectMeta.LastAttemptStatus != metrix.CollectStatusSuccess {
142 sample.skippedFailed = true
143 e.logDebugf("chartengine build skipped: collect status=%d", collectMeta.LastAttemptStatus)
144 return out, materializedState{}, 0, 0, 0, false, nil
145 }
146
147 obs := e.observeBuildSuccessSeq(collectMeta.LastSuccessSeq)
148 buildCycle := e.nextBuildCycle(collectMeta.LastSuccessSeq)
149 sample.buildSeqViolation = e.state.buildSeq.violating
150 sample.buildSeqObserved = true
151 switch obs.transition {
152 case buildSeqTransitionBroken:
153 sample.buildSeqBroken = true
154 e.logWarningf(
155 "chartengine build sequence is non-monotonic: current=%d previous=%d (suppressing repeats until recovery)",
156 collectMeta.LastSuccessSeq,
157 obs.previous,
158 )
159 case buildSeqTransitionRecovered:
160 sample.buildSeqRecovered = true
161 e.logInfof(
162 "chartengine build sequence monotonicity recovered: current=%d previous=%d",
163 collectMeta.LastSuccessSeq,
164 obs.previous,
165 )
166 }
167
168 phaseStartedAt := time.Now()
169 staged := e.state.materialized.clone()
170 ctx, err := e.preparePlanBuildContext(reader, &out, collectMeta, buildCycle, &staged)
171 sample.phasePrepareSeconds = time.Since(phaseStartedAt).Seconds()
172 if err != nil {
173 sample.buildErr = true
174 e.logWarningf("chartengine build prepare failed: %v", err)
175 return Plan{}, materializedState{}, 0, 0, 0, false, err
176 }
177 phaseStartedAt = time.Now()
178 if err := validateBuildReaderForInferredDimensions(ctx.index, reader); err != nil {
179 sample.phaseValidateSeconds = time.Since(phaseStartedAt).Seconds()
180 sample.buildErr = true
181 e.logWarningf("chartengine build reader validation failed: %v", err)
182 return Plan{}, materializedState{}, 0, 0, 0, false, err
183 }
184 sample.phaseValidateSeconds = time.Since(phaseStartedAt).Seconds()
185 phaseStartedAt = time.Now()
186 if err := e.scanPlanSeries(ctx); err != nil {
187 sample.phaseScanSeconds = time.Since(phaseStartedAt).Seconds()
188 sample.buildErr = true
189 e.logWarningf("chartengine build scan failed: %v", err)
190 return Plan{}, materializedState{}, 0, 0, 0, false, err
191 }
192 sample.phaseScanSeconds = time.Since(phaseStartedAt).Seconds()
193
194 // Route-cache lifecycle follows metrix snapshot membership.
195 phaseStartedAt = time.Now()
196 retainStats := ctx.cache.RetainSeen(ctx.collectMeta.LastSuccessSeq)
197 sample.phaseRetainSeconds = time.Since(phaseStartedAt).Seconds()
198 sample.routeCacheEntries = retainStats.EntriesAfter
199 sample.routeCacheRetained = retainStats.EntriesAfter
200 sample.routeCachePruned = retainStats.Pruned
201 sample.routeCacheFullDrop = retainStats.FullDrop
202
203 phaseStartedAt = time.Now()
204 removeByCapDims, removeByCapCharts := enforceLifecycleCaps(ctx.collectMeta.LastSuccessSeq, ctx.chartsByID, ctx.materialized)
205 sample.phaseLifecycleCapsSec = time.Since(phaseStartedAt).Seconds()
206 sample.lifecycleRemovedDimensionByCap = len(removeByCapDims)
207 sample.lifecycleRemovedChartByCap = len(removeByCapCharts)
208 for _, action := range removeByCapDims {
209 out.Actions = append(out.Actions, action)
210 }
211 for _, action := range removeByCapCharts {
212 out.Actions = append(out.Actions, action)
213 }
214 phaseStartedAt = time.Now()
215 if err := e.materializePlanCharts(ctx); err != nil {
216 sample.phaseMaterializeSeconds = time.Since(phaseStartedAt).Seconds()
217 sample.buildErr = true
218 e.logWarningf("chartengine build materialization failed: %v", err)
219 return Plan{}, materializedState{}, 0, 0, 0, false, err
220 }
221 sample.phaseMaterializeSeconds = time.Since(phaseStartedAt).Seconds()
222 phaseStartedAt = time.Now()
223 removeDims, removeCharts := collectExpiryRemovals(ctx.collectMeta.LastSuccessSeq, ctx.materialized)
224 sample.phaseExpirySeconds = time.Since(phaseStartedAt).Seconds()
225 sample.lifecycleRemovedDimensionByExpiry = len(removeDims)
226 sample.lifecycleRemovedChartByExpiry = len(removeCharts)
227 for _, action := range removeDims {
228 out.Actions = append(out.Actions, action)
229 }
230 for _, action := range removeCharts {
231 out.Actions = append(out.Actions, action)
232 }
233 phaseStartedAt = time.Now()
234 sortInferredDimensions(out.InferredDimensions)
235 sample.phaseSortSeconds = time.Since(phaseStartedAt).Seconds()
236
237 sample.planRouteStats = ctx.planRouteStats
238 sample.planChartInstances = len(ctx.chartsByID)
239 sample.planInferredDimensions = len(out.InferredDimensions)
240
241 actionCounts := actionKindCounts(out.Actions)
242 sample.actionCreateChart = actionCounts.actionCreateChart
243 sample.actionCreateDimension = actionCounts.actionCreateDimension
244 sample.actionUpdateChart = actionCounts.actionUpdateChart
245 sample.actionRemoveDimension = actionCounts.actionRemoveDimension
246 sample.actionRemoveChart = actionCounts.actionRemoveChart
247 sample.buildSuccess = true
248 e.state.hints.chartsByID = len(ctx.chartsByID)
249 e.state.hints.seenInfer = len(ctx.seenInfer)
250
251 attemptID := e.nextAttemptIDLocked()
252 e.state.outstanding = attemptID
253 return out, staged, e.state.engineEpoch, e.state.commitSeq, attemptID, true, nil
254 }
255
256 func validateBuildReaderForInferredDimensions(index matchIndex, reader metrix.Reader) error {
257 templateID, dimIndex, requiresFlatten := firstInferDimension(index)
258 if !requiresFlatten {
259 return nil
260 }
261 aware, ok := reader.(flattenedReadChecker)
262 if ok && aware.FlattenedRead() {
263 return nil
264 }
265 return fmt.Errorf(
266 "chartengine: inferred dimension requires flattened reader metadata (template_id=%q dim_index=%d); use store.Read(metrix.ReadFlatten())",
267 templateID,
268 dimIndex,
269 )
270 }
271
272 func firstInferDimension(index matchIndex) (string, int, bool) {
273 if len(index.chartsByID) == 0 {
274 return "", 0, false
275 }
276 templateIDs := make([]string, 0, len(index.chartsByID))
277 for templateID := range index.chartsByID {
278 templateIDs = append(templateIDs, templateID)
279 }
280 sort.Strings(templateIDs)
281 for _, templateID := range templateIDs {
282 chart := index.chartsByID[templateID]
283 for i := range chart.Dimensions {
284 if chart.Dimensions[i].InferNameFromSeriesMeta {
285 return templateID, i, true
286 }
287 }
288 }
289 return "", 0, false
290 }
291
292 func (e *Engine) preparePlanBuildContext(
293 reader metrix.Reader,
294 out *Plan,
295 collectMeta metrix.CollectMeta,
296 buildCycle uint64,
297 materialized *materializedState,
298 ) (*planBuildContext, error) {
299 prog := e.state.program
300 if prog == nil {
301 return nil, fmt.Errorf("chartengine: no compiled program loaded")
302 }
303 cache := e.state.routeCache
304 if cache == nil {
305 cache = newRouteCache()
306 e.state.routeCache = cache
307 }
308 if materialized == nil {
309 return nil, fmt.Errorf("chartengine: nil materialized state")
310 }
311 if materialized.charts == nil {
312 *materialized = newMaterializedState()
313 }
314 index := e.state.matchIndex
315 if index.chartsByID == nil {
316 index = buildMatchIndex(prog.Charts())
317 e.state.matchIndex = index
318 }
319 chartOwners := make(map[string]string, len(materialized.charts))
320 dimCapHints := make(map[string]int, len(materialized.charts))
321 for chartID, matChart := range materialized.charts {
322 chartOwners[chartID] = matChart.templateID
323 if n := len(matChart.dimensions); n > 0 {
324 dimCapHints[chartID] = n
325 }
326 }
327 chartsCap := max(e.state.hints.chartsByID, len(materialized.charts))
328 seenInferCap := e.state.hints.seenInfer
329 return &planBuildContext{
330 out: out,
331 reader: reader,
332 collectMeta: collectMeta,
333 buildCycle: buildCycle,
334 prog: prog,
335 cache: cache,
336 index: index,
337 flat: reader,
338 seenInfer: make(map[string]struct{}, seenInferCap),
339 chartsByID: make(map[string]*chartState, chartsCap),
340 chartOwners: chartOwners,
341 dimCapHints: dimCapHints,
342 materialized: materialized,
343 materializedByID: materialized.charts,
344 }, nil
345 }
346
347 func (e *Engine) scanPlanSeries(ctx *planBuildContext) error {
348 var firstErr error
349 buildSeq := ctx.collectMeta.LastSuccessSeq
350 process := func(identity metrix.SeriesIdentity, meta metrix.SeriesMeta, name string, labels metrix.LabelView, v metrix.SampleValue) {
351 ctx.seriesScanned++
352 if firstErr != nil {
353 return
354 }
355 if e.state.cfg.seriesSelection == seriesSelectionLastSuccessOnly &&
356 meta.LastSeenSuccessSeq != ctx.collectMeta.LastSuccessSeq {
357 ctx.seriesFilteredBySeq++
358 ctx.cache.MarkSeenIfPresent(identity, buildSeq)
359 return
360 }
361 if selector := e.state.cfg.selector; selector != nil && !selector.Matches(name, labels) {
362 ctx.seriesFilteredBySel++
363 ctx.cache.MarkSeenIfPresent(identity, buildSeq)
364 return
365 }
366
367 routes, hit, err := e.resolveSeriesRoutes(
368 ctx.cache,
369 identity,
370 name,
371 labels,
372 meta,
373 ctx.index,
374 ctx.prog.Revision(),
375 buildSeq,
376 )
377 if err != nil {
378 firstErr = err
379 return
380 }
381 if hit {
382 e.addRouteCacheHit()
383 ctx.routeCacheHits++
384 } else {
385 e.addRouteCacheMiss()
386 ctx.routeCacheMisses++
387 }
388 if len(routes) == 0 {
389 autoRoutes, ok, err := e.resolveAutogenRoute(ctx.reader, name, labels, meta)
390 if err != nil {
391 firstErr = err
392 return
393 }
394 if ok {
395 routes = autoRoutes
396 ctx.seriesAutogenMatched++
397 ctx.seriesMatched++
398 } else {
399 ctx.seriesUnmatched++
400 return
401 }
402 } else {
403 ctx.seriesMatched++
404 }
405
406 for _, route := range routes {
407 if err := ctx.accumulateRoute(ctx.index, route, labels, v); err != nil {
408 firstErr = err
409 return
410 }
411 }
412 }
413
414 if rawIter, ok := ctx.flat.(metrix.SeriesIdentityRawIterator); ok {
415 view := &labelSliceView{}
416 rawIter.ForEachSeriesIdentityRaw(func(identity metrix.SeriesIdentity, meta metrix.SeriesMeta, name string, labels []metrix.Label, v metrix.SampleValue) {
417 view.items = labels
418 process(identity, meta, name, view, v)
419 })
420 return firstErr
421 }
422
423 ctx.flat.ForEachSeriesIdentity(func(identity metrix.SeriesIdentity, meta metrix.SeriesMeta, name string, labels metrix.LabelView, v metrix.SampleValue) {
424 process(identity, meta, name, labels, v)
425 })
426 return firstErr
427 }
428
429 func (ctx *planBuildContext) accumulateRoute(
430 index matchIndex,
431 route routeBinding,
432 labels metrix.LabelView,
433 value metrix.SampleValue,
434 ) error {
435 cs, exists := ctx.chartsByID[route.ChartID]
436 if exists && cs.templateID != route.ChartTemplateID {
437 if !route.Autogen && isAutogenTemplateID(cs.templateID) {
438 // Template wins over autogen on chart-id collision.
439 ctx.chartOwners[route.ChartID] = route.ChartTemplateID
440 delete(ctx.chartsByID, route.ChartID)
441 cs = nil
442 exists = false
443 } else {
444 // Cross-template rendered-id collision.
445 // Existing owner keeps chart-id ownership.
446 return nil
447 }
448 }
449 if !exists {
450 ownerTemplateID, ownerExists := ctx.chartOwners[route.ChartID]
451 if ownerExists && ownerTemplateID != route.ChartTemplateID {
452 if !route.Autogen && isAutogenTemplateID(ownerTemplateID) {
453 // Template wins over autogen on chart-id collision.
454 ctx.chartOwners[route.ChartID] = route.ChartTemplateID
455 delete(ctx.chartsByID, route.ChartID)
456 } else {
457 // Cross-template rendered-id collision.
458 // Existing owner keeps chart-id ownership.
459 return nil
460 }
461 }
462 if !ownerExists {
463 ctx.chartOwners[route.ChartID] = route.ChartTemplateID
464 }
465
466 dimCap := ctx.dimCapHints[route.ChartID]
467 var entries map[string]*dimBuildEntry
468 var labelsAcc *chartLabelAccumulator
469 if matChart := ctx.materializedByID[route.ChartID]; matChart != nil {
470 entries = matChart.checkoutScratchEntries(dimCap)
471 // Labels are only emitted on chart creation, so skip observe work for existing charts.
472 labelsAcc = nil
473 } else {
474 entries = make(map[string]*dimBuildEntry, dimCap)
475 labelsAcc = newAutogenChartLabelAccumulator()
476 if !route.Autogen {
477 chart, ok := index.chartsByID[route.ChartTemplateID]
478 if !ok {
479 return fmt.Errorf("chartengine: route references unknown chart template %q", route.ChartTemplateID)
480 }
481 labelsAcc = newChartLabelAccumulator(chart)
482 }
483 }
484 cs = &chartState{
485 templateID: route.ChartTemplateID,
486 chartID: route.ChartID,
487 meta: route.Meta,
488 lifecycle: route.Lifecycle,
489 labels: labelsAcc,
490 entries: entries,
491 currentBuildSeq: ctx.buildCycle,
492 }
493 ctx.chartsByID[route.ChartID] = cs
494 }
495
496 entry, exists := cs.entries[route.DimensionName]
497 if !exists {
498 entry = &dimBuildEntry{}
499 cs.entries[route.DimensionName] = entry
500 }
501 if entry.seenSeq != cs.currentBuildSeq {
502 entry.seenSeq = cs.currentBuildSeq
503 entry.value = value
504 entry.dimensionState = dimensionState{
505 hidden: route.Hidden,
506 float: route.Float,
507 static: route.Static,
508 order: route.DimensionIndex,
509 algorithm: route.Algorithm,
510 multiplier: route.Multiplier,
511 divisor: route.Divisor,
512 }
513 cs.observedCount++
514 } else {
515 if entry.hidden != route.Hidden {
516 // First-observed hidden flag wins within one build; conflicting routes are ignored.
517 }
518 if entry.float != route.Float {
519 // First-observed float flag wins within one build; conflicting routes are ignored.
520 }
521 entry.value += value
522 }
523
524 if cs.labels != nil {
525 if err := cs.labels.observe(labels, route.DimensionKeyLabel); err != nil {
526 return err
527 }
528 }
529
530 if route.Inferred {
531 key := fmt.Sprintf("%s\xff%d\xff%s", route.ChartTemplateID, route.DimensionIndex, route.DimensionName)
532 if _, exists := ctx.seenInfer[key]; !exists {
533 ctx.seenInfer[key] = struct{}{}
534 ctx.out.InferredDimensions = append(ctx.out.InferredDimensions, InferredDimension{
535 ChartTemplateID: route.ChartTemplateID,
536 DimensionIndex: route.DimensionIndex,
537 Name: route.DimensionName,
538 })
539 }
540 }
541 return nil
542 }
543
544 func (e *Engine) materializePlanCharts(ctx *planBuildContext) error {
545 chartIDs := make([]string, 0, len(ctx.chartsByID))
546 for chartID, cs := range ctx.chartsByID {
547 if cs.observedCount == 0 {
548 continue
549 }
550 chartIDs = append(chartIDs, chartID)
551 }
552 sort.Strings(chartIDs)
553
554 for _, chartID := range chartIDs {
555 cs := ctx.chartsByID[chartID]
556 matChart, chartCreated := ctx.materialized.ensureChart(cs.chartID, cs.templateID, cs.meta, cs.lifecycle)
557 if chartCreated {
558 chartLabels := map[string]string(nil)
559 if cs.labels != nil {
560 labels, err := cs.labels.materialize()
561 if err != nil {
562 return err
563 }
564 chartLabels = labels
565 }
566 ctx.out.Actions = append(ctx.out.Actions, CreateChartAction{
567 ChartTemplateID: cs.templateID,
568 ChartID: cs.chartID,
569 Meta: cs.meta,
570 Labels: chartLabels,
571 })
572 }
573 matChart.lastSeenSuccessSeq = ctx.collectMeta.LastSuccessSeq
574
575 observedNames := observedDimensionNames(cs, matChart)
576 for _, name := range observedNames {
577 entry := cs.entries[name]
578 if entry == nil || entry.seenSeq != cs.currentBuildSeq {
579 continue
580 }
581 matDim, dimCreated := matChart.ensureDimension(name, entry.dimensionState)
582 if dimCreated {
583 ctx.out.Actions = append(ctx.out.Actions, CreateDimensionAction{
584 ChartID: cs.chartID,
585 ChartMeta: cs.meta,
586 Name: name,
587 Hidden: entry.hidden,
588 Float: entry.float,
589 Algorithm: entry.algorithm,
590 Multiplier: entry.multiplier,
591 Divisor: entry.divisor,
592 })
593 }
594 matDim.lastSeenSuccessSeq = ctx.collectMeta.LastSuccessSeq
595 }
596
597 updateNames := matChart.orderedDimensionNames()
598 values := make([]UpdateDimensionValue, 0, len(updateNames))
599 for _, name := range updateNames {
600 entry, ok := cs.entries[name]
601 if ok && entry != nil && entry.seenSeq == cs.currentBuildSeq {
602 if math.IsNaN(entry.value) || math.IsInf(entry.value, 0) {
603 // A non-finite value (e.g. a summary quantile with no observations this
604 // cycle) must render as a gap, not 0: emit SETEMPTY rather than carry NaN.
605 values = append(values, UpdateDimensionValue{Name: name, IsEmpty: true})
606 continue
607 }
608 values = append(values, UpdateDimensionValue{
609 Name: name,
610 IsFloat: entry.float,
611 Int64: int64(entry.value),
612 Float64: entry.value,
613 })
614 continue
615 }
616 values = append(values, UpdateDimensionValue{
617 Name: name,
618 IsEmpty: true,
619 })
620 }
621 ctx.out.Actions = append(ctx.out.Actions, UpdateChartAction{
622 ChartID: cs.chartID,
623 Values: values,
624 })
625 matChart.storeScratchEntries(cs.entries)
626 matChart.pruneScratchEntries(cs.currentBuildSeq)
627 }
628 return nil
629 }
630
631 func observedDimensionNames(cs *chartState, matChart *materializedChartState) []string {
632 if cs == nil {
633 return nil
634 }
635 if cs.observedCount == 0 {
636 return nil
637 }
638 if matChart == nil || len(matChart.dimensions) == 0 {
639 return orderedObservedDimensionNames(cs.entries, cs.currentBuildSeq)
640 }
641 prev := matChart.orderedDimensionNames()
642 if len(prev) != cs.observedCount {
643 return orderedObservedDimensionNames(cs.entries, cs.currentBuildSeq)
644 }
645 for _, name := range prev {
646 entry, ok := cs.entries[name]
647 if !ok || entry == nil || entry.seenSeq != cs.currentBuildSeq {
648 return orderedObservedDimensionNames(cs.entries, cs.currentBuildSeq)
649 }
650 existing := matChart.dimensions[name]
651 if existing == nil || existing.static != entry.static || existing.order != entry.order {
652 return orderedObservedDimensionNames(cs.entries, cs.currentBuildSeq)
653 }
654 }
655 return prev
656 }
657
658 func sortInferredDimensions(in []InferredDimension) {
659 sort.Slice(in, func(i, j int) bool {
660 lhs := in[i]
661 rhs := in[j]
662 if lhs.ChartTemplateID != rhs.ChartTemplateID {
663 return lhs.ChartTemplateID < rhs.ChartTemplateID
664 }
665 if lhs.DimensionIndex != rhs.DimensionIndex {
666 return lhs.DimensionIndex < rhs.DimensionIndex
667 }
668 return lhs.Name < rhs.Name
669 })
670 }
671
672 func isAutogenTemplateID(templateID string) bool {
673 return strings.HasPrefix(templateID, autogenTemplatePrefix)
674 }
675
676 func (e *Engine) nextBuildCycle(sourceSuccessSeq uint64) uint64 {
677 if !e.state.cfg.runtimePlanner {
678 return sourceSuccessSeq
679 }
680 e.state.plannerBuildSeq++
681 // Seen-seq zero value is reserved for "never seen".
682 if e.state.plannerBuildSeq == 0 {
683 e.state.plannerBuildSeq = 1
684 }
685 return e.state.plannerBuildSeq
686 }