coverage~bigbes/sr-ht-dolt3523280cbeads/cache.go

Coverage
88.0% 22/25 statements
Δ
+0.0
Blob
b5d2695
1 package beads
2
3 import (
4 "sync"
5 "time"
6 )
7
8 // --- the cache the cross-database readings share ------------------------------
9 //
10 // Two readings here open every database one caller may browse: /ready's
11 // aggregation (ReadyAcross) and the prefix index behind cross-database issue
12 // links (PrefixesAcross). Opening N stores per request is exactly what the
13 // per-request browse discipline does not scale to, and both are bounded the same
14 // way:
15 //
16 // 1. A head-hash gate. Opening a session and listing branches is cheap; reading
17 // and projecting rows is not. When the head has not moved, the cached
18 // projection stands and no row is read.
19 // 2. A TTL (ReadyCacheTTL), so a cache can never be the reason a reader sees
20 // yesterday's answer.
21 // 3. A ceiling on how many databases one call opens (ReadyMaxDatabases),
22 // applied by the aggregations themselves.
23 //
24 // The bounds are one set of numbers, named in ready.go where /ready needed them
25 // first. What differs between the two readings is only the projection being
26 // cached, which is what the type parameter is: a second cache with its own
27 // lifetime rules is how two pages start disagreeing about how fresh "fresh" is.
28 //
29 // What is cached is always a projection and never an open session. An open store
30 // is a file handle and a memory mapping; caching those is the thing per-request
31 // opening exists to prevent.
32
33 // cached is one database's cached projection plus the two facts the bounds are
34 // checked against: the head it was read at, and when it was stored.
35 type cached[T any] struct {
36 head string
37 at time.Time
38 value T
39 }
40
41 // projectionCache is a small mutex-guarded map from repository id to one cached
42 // projection. The repository id is the identity it is keyed on: no two databases
43 // share it and it survives a rename.
44 //
45 // The zero value is usable — the map is allocated on first store — so a cache
46 // can be a field of a value that has no constructor.
47 type projectionCache[T any] struct {
48 mu sync.Mutex
49 entries map[int]cached[T]
50 }
51
52 // lookup returns the cached projection for a database when it was read at the
53 // same head and has not expired. Both conditions, not either: the head hash is
54 // what makes it correct, the TTL is what makes it bounded.
55 209 func (c *projectionCache[T]) lookup(id int, head string, now time.Time) (T, bool) {
56 209 var zero T
57 209 if head == "" {
58 0 // A database whose head cannot be named cannot be gated on one.
59 0 return zero, false
60 0 }
61 209 c.mu.Lock()
62 209 defer c.mu.Unlock()
63 209 e, ok := c.entries[id]
64 209 if !ok || e.head != head || now.Sub(e.at) >= ReadyCacheTTL {
65 193 return zero, false
66 193 }
67 16 return e.value, true
68 }
69
70 // store records a projection read at head, dropping expired entries — and, if
71 // that was not enough, everything — when the map is at its ceiling. A cache is
72 // not a store: over the ceiling it starts again rather than growing with the
73 // instance.
74 459 func (c *projectionCache[T]) store(id int, head string, now time.Time, value T) {
75 459 if head == "" {
76 0 return
77 0 }
78 459 c.mu.Lock()
79 459 defer c.mu.Unlock()
80 459 if c.entries == nil {
81 40 c.entries = make(map[int]cached[T])
82 40 }
83 459 if len(c.entries) >= readyCacheMaxEntries {
84 256 for k, old := range c.entries {
85 256 if now.Sub(old.at) >= ReadyCacheTTL {
86 0 delete(c.entries, k)
87 0 }
88 }
89 1 if len(c.entries) >= readyCacheMaxEntries {
90 1 c.entries = make(map[int]cached[T], readyCacheMaxEntries)
91 1 }
92 }
93 459 c.entries[id] = cached[T]{head: head, at: now, value: value}
94 }
95
96 // size reports how many entries the cache holds. It exists for the tests that
97 // assert the ceiling: the bound is the point of the map, and only a count can
98 // check it.
99 1 func (c *projectionCache[T]) size() int {
100 1 c.mu.Lock()
101 1 defer c.mu.Unlock()
102 1 return len(c.entries)
103 1 }