master
go 179 lines 4.3 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package cache
4
5 import (
6 "github.com/netdata/netdata/go/plugins/pkg/metrix"
7 )
8
9 type routeCacheEntry[T any] struct {
10 identity metrix.SeriesIdentity
11 revision uint64
12 values []T
13 // lastSeenBuild tracks last successful build sequence that observed this series.
14 lastSeenBuild uint64
15 }
16
17 type RetainSeenStats struct {
18 EntriesBefore int
19 EntriesAfter int
20 Pruned int
21 FullDrop bool
22 }
23
24 // RouteCache stores resolved routes keyed by metrix series identity.
25 //
26 // The cache is intentionally unbounded and is pruned by RetainSeen(), so
27 // lifecycle remains aligned with metrix retention/source-of-truth.
28 // Synchronization is intentionally delegated to chartengine, which serializes
29 // BuildPlan/Load state transitions under Engine.mu.
30 type RouteCache[T any] struct {
31 buckets map[uint64][]routeCacheEntry[T]
32
33 // seenBuild is the build sequence currently tracked by seenCount.
34 seenBuild uint64
35 // seenCount is number of cache entries marked seen in seenBuild.
36 seenCount int
37 // entryCount is total number of cache entries.
38 entryCount int
39 }
40
41 func NewRouteCache[T any]() *RouteCache[T] {
42 return &RouteCache[T]{
43 buckets: make(map[uint64][]routeCacheEntry[T]),
44 }
45 }
46
47 func (c *RouteCache[T]) Lookup(identity metrix.SeriesIdentity, revision uint64, buildSeq uint64) ([]T, bool) {
48 bucket := c.buckets[identity.Hash64]
49 for i := range bucket {
50 if bucket[i].identity.ID != identity.ID {
51 continue
52 }
53 if bucket[i].revision != revision {
54 return nil, false
55 }
56 c.markSeen(&bucket[i], buildSeq)
57 // Return immutable cached values directly to avoid per-lookup allocations.
58 // Callers must treat the returned slice as read-only.
59 return bucket[i].values, true
60 }
61 return nil, false
62 }
63
64 func (c *RouteCache[T]) MarkSeenIfPresent(identity metrix.SeriesIdentity, buildSeq uint64) {
65 bucket := c.buckets[identity.Hash64]
66 for i := range bucket {
67 if bucket[i].identity.ID != identity.ID {
68 continue
69 }
70 c.markSeen(&bucket[i], buildSeq)
71 return
72 }
73 }
74
75 func (c *RouteCache[T]) Store(identity metrix.SeriesIdentity, revision uint64, buildSeq uint64, values []T) {
76 if !c.beginBuild(buildSeq) {
77 return
78 }
79 bucket := c.buckets[identity.Hash64]
80 for i := range bucket {
81 if bucket[i].identity.ID != identity.ID {
82 continue
83 }
84 bucket[i].revision = revision
85 bucket[i].values = cloneSlice(values)
86 if bucket[i].lastSeenBuild != buildSeq {
87 bucket[i].lastSeenBuild = buildSeq
88 c.seenCount++
89 }
90 c.buckets[identity.Hash64] = bucket
91 return
92 }
93
94 entry := routeCacheEntry[T]{
95 identity: identity,
96 revision: revision,
97 values: cloneSlice(values),
98 lastSeenBuild: buildSeq,
99 }
100 c.buckets[identity.Hash64] = append(bucket, entry)
101 c.entryCount++
102 c.seenCount++
103 }
104
105 // RetainSeen keeps entries that were observed in current successful build sequence.
106 func (c *RouteCache[T]) RetainSeen(buildSeq uint64) RetainSeenStats {
107 stats := RetainSeenStats{
108 EntriesBefore: c.entryCount,
109 EntriesAfter: c.entryCount,
110 }
111 if c.entryCount == 0 {
112 return stats
113 }
114 if buildSeq < c.seenBuild {
115 return stats
116 }
117 if c.seenBuild != buildSeq {
118 // No cached entries were observed in this build; drop all.
119 clear(c.buckets)
120 c.entryCount = 0
121 c.seenBuild = buildSeq
122 c.seenCount = 0
123 stats.EntriesAfter = 0
124 stats.Pruned = stats.EntriesBefore
125 stats.FullDrop = true
126 return stats
127 }
128 if c.seenCount == c.entryCount {
129 // Steady-state fast path: all cached entries were seen this build.
130 return stats
131 }
132
133 keptTotal := 0
134 for hash, bucket := range c.buckets {
135 kept := retainSeenEntries(bucket, buildSeq)
136 if len(kept) == 0 {
137 delete(c.buckets, hash)
138 continue
139 }
140 c.buckets[hash] = kept
141 keptTotal += len(kept)
142 }
143 c.entryCount = keptTotal
144 c.seenCount = keptTotal
145 stats.EntriesAfter = keptTotal
146 stats.Pruned = stats.EntriesBefore - keptTotal
147 return stats
148 }
149
150 func (c *RouteCache[T]) markSeen(entry *routeCacheEntry[T], buildSeq uint64) {
151 if !c.beginBuild(buildSeq) {
152 return
153 }
154 if entry.lastSeenBuild == buildSeq {
155 return
156 }
157 entry.lastSeenBuild = buildSeq
158 c.seenCount++
159 }
160
161 func (c *RouteCache[T]) beginBuild(buildSeq uint64) bool {
162 if buildSeq < c.seenBuild {
163 return false
164 }
165 if c.seenBuild != buildSeq {
166 c.seenBuild = buildSeq
167 c.seenCount = 0
168 }
169 return true
170 }
171
172 func cloneSlice[T any](in []T) []T {
173 if len(in) == 0 {
174 return nil
175 }
176 out := make([]T, len(in))
177 copy(out, in)
178 return out
179 }