cloudflare/pint

Public

mirrored from https://github.com/cloudflare/pintAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.33.0

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

internal/promapi/cache.go

256lines · modecode

1package promapi
2
3import (
4 "sort"
5 "sync"
6 "time"
7
8 "github.com/prometheus/client_golang/prometheus"
9)
10
11type cacheEntry struct {
12 data queryResult
13 expiresAt time.Time
14 lastGet time.Time
15 cost int
16 gets int
17}
18
19type cacheLine struct {
20 key uint64
21 val *cacheEntry
22 ttl time.Duration
23 isExpired bool
24}
25
26type cacheLines []cacheLine
27
28func (cl cacheLines) Len() int {
29 return len(cl)
30}
31
32func (cl cacheLines) Swap(i, j int) {
33 cl[i], cl[j] = cl[j], cl[i]
34}
35
36// [expired, costly, cheap, high ttl]
37func (cl cacheLines) Less(i, j int) bool {
38 if cl[i].isExpired != cl[j].isExpired {
39 return cl[i].isExpired
40 }
41
42 if cl[i].val.cost != cl[j].val.cost {
43 if cl[i].val.gets == 0 || cl[j].val.gets == 0 {
44 return cl[i].val.cost >= cl[j].val.cost
45 }
46
47 ca := float64(cl[i].val.cost) / float64(cl[i].val.gets)
48 cb := float64(cl[j].val.cost) / float64(cl[j].val.gets)
49 if ca != cb {
50 return ca >= cb
51 }
52 }
53
54 return cl[i].ttl < cl[j].ttl
55}
56
57type endpointStats struct {
58 hits int
59 misses int
60}
61
62func (e *endpointStats) hit() { e.hits++ }
63func (e *endpointStats) miss() { e.misses++ }
64
65func newQueryCache(maxSize int, maxStale time.Duration, maxEntry float64) *queryCache {
66 return &queryCache{
67 entries: map[uint64]*cacheEntry{},
68 stats: map[string]*endpointStats{},
69 maxCost: maxSize,
70 maxStale: maxStale,
71 maxEntry: int(float64(maxSize) * maxEntry),
72 }
73}
74
75type queryCache struct {
76 mu sync.Mutex
77 entries map[uint64]*cacheEntry
78 stats map[string]*endpointStats
79 maxStale time.Duration
80 maxEntry int
81 cost int
82 maxCost int
83 evictions int
84}
85
86func (c *queryCache) endpointStats(endpoint string) *endpointStats {
87 e, ok := c.stats[endpoint]
88 if ok {
89 return e
90 }
91
92 e = &endpointStats{}
93 c.stats[endpoint] = e
94 return e
95}
96
97func (c *queryCache) get(key uint64, endpoint string) (v queryResult, ok bool) {
98 c.mu.Lock()
99 defer c.mu.Unlock()
100
101 var ce *cacheEntry
102 ce, ok = c.entries[key]
103 if !ok {
104 c.endpointStats(endpoint).miss()
105 return v, ok
106 }
107
108 ce.gets++
109 c.endpointStats(endpoint).hit()
110
111 ce.lastGet = time.Now()
112 return ce.data, true
113}
114
115// Cache results if it was requested at least twice EVER - which means it's either
116// popular and requested multiple times within a loop OR this cache key survives between loops.
117func (c *queryCache) set(key uint64, val queryResult, ttl time.Duration, cost int, endpoint string) {
118 c.mu.Lock()
119 defer c.mu.Unlock()
120
121 if cost > c.maxEntry {
122 return
123 }
124
125 oe, ok := c.entries[key]
126 if ok {
127 c.cost -= oe.cost
128 }
129
130 // If we're not updating in-place then we need to make room for this entry
131 if !ok && c.cost+cost > c.maxCost {
132 c.makeRoom(cost)
133 }
134
135 c.cost += cost
136 c.entries[key] = &cacheEntry{
137 data: val,
138 cost: cost,
139 }
140 if ttl > 0 {
141 c.entries[key].expiresAt = time.Now().Add(ttl)
142 }
143}
144
145func (c *queryCache) makeRoom(needed int) {
146 now := time.Now()
147 purgeEmptyBefore := now.Add(c.maxStale * -1)
148 for key, ce := range c.entries {
149 if (!ce.expiresAt.IsZero() && ce.expiresAt.Before(now)) || ce.lastGet.Before(purgeEmptyBefore) {
150 c.cost -= ce.cost
151 needed -= ce.cost
152 delete(c.entries, key)
153 c.evictions++
154 }
155 }
156 if needed <= 0 {
157 return
158 }
159
160 entries := make(cacheLines, 0, len(c.entries))
161 for key, ce := range c.entries {
162 entries = append(entries, cacheLine{
163 key: key,
164 val: ce,
165 ttl: ce.expiresAt.Sub(now).Round(time.Second),
166 isExpired: ce.expiresAt.Before(now),
167 })
168 }
169 sort.Stable(entries)
170
171 for i := len(entries) - 1; i >= 0; i-- {
172 c.cost -= entries[i].val.cost
173 needed -= entries[i].val.cost
174 delete(c.entries, entries[i].key)
175 c.evictions++
176 if needed <= 0 {
177 return
178 }
179 }
180}
181
182func (c *queryCache) gc() {
183 c.mu.Lock()
184 defer c.mu.Unlock()
185
186 entries := map[uint64]*cacheEntry{}
187
188 now := time.Now()
189 purgeEmptyBefore := now.Add(c.maxStale * -1)
190 for key, ce := range c.entries {
191 if ce.lastGet.Before(purgeEmptyBefore) || (!ce.expiresAt.IsZero() && ce.expiresAt.Before(now)) {
192 c.cost -= ce.cost
193 c.evictions++
194 continue
195 }
196 entries[key] = ce
197 }
198 c.entries = entries
199}
200
201type cacheCollector struct {
202 cache *queryCache
203 entries *prometheus.Desc
204 hits *prometheus.Desc
205 misses *prometheus.Desc
206 evictions *prometheus.Desc
207}
208
209func newCacheCollector(cache *queryCache, name string) *cacheCollector {
210 return &cacheCollector{
211 cache: cache,
212 entries: prometheus.NewDesc(
213 "pint_prometheus_cache_size",
214 "Total number of entries currently stored in Prometheus query cache",
215 nil,
216 prometheus.Labels{"name": name},
217 ),
218 hits: prometheus.NewDesc(
219 "pint_prometheus_cache_hits_total",
220 "Total number of query cache hits",
221 []string{"endpoint"},
222 prometheus.Labels{"name": name},
223 ),
224 misses: prometheus.NewDesc(
225 "pint_prometheus_cache_miss_total",
226 "Total number of query cache misses",
227 []string{"endpoint"},
228 prometheus.Labels{"name": name},
229 ),
230 evictions: prometheus.NewDesc(
231 "pint_prometheus_cache_evictions_total",
232 "Total number of times an entry was evicted from query cache due to size limit or TTL",
233 nil,
234 prometheus.Labels{"name": name},
235 ),
236 }
237}
238
239func (c *cacheCollector) Describe(ch chan<- *prometheus.Desc) {
240 ch <- c.entries
241 ch <- c.hits
242 ch <- c.misses
243 ch <- c.evictions
244}
245
246func (c *cacheCollector) Collect(ch chan<- prometheus.Metric) {
247 c.cache.mu.Lock()
248 defer c.cache.mu.Unlock()
249 ch <- prometheus.MustNewConstMetric(c.entries, prometheus.GaugeValue, float64(c.cache.cost))
250
251 for endpoint, stats := range c.cache.stats {
252 ch <- prometheus.MustNewConstMetric(c.hits, prometheus.CounterValue, float64(stats.hits), endpoint)
253 ch <- prometheus.MustNewConstMetric(c.misses, prometheus.CounterValue, float64(stats.misses), endpoint)
254 }
255 ch <- prometheus.MustNewConstMetric(c.evictions, prometheus.CounterValue, float64(c.cache.evictions))
256}
257