Skip to content

Commit 9dc6d32

Browse files
committed
cache: generic cache implementation
Signed-off-by: Hank Donnay <[email protected]>
1 parent 866b17b commit 9dc6d32

File tree

2 files changed

+88
-0
lines changed

2 files changed

+88
-0
lines changed

internal/cache/cache.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
//go:build go1.24
2+
3+
package cache
4+
5+
import (
6+
"context"
7+
"runtime"
8+
"sync"
9+
"unique"
10+
"weak"
11+
12+
"github.com/quay/claircore/internal/singleflight"
13+
)
14+
15+
// Live is a cache that keeps a cached copy as long as the go runtime determines
16+
// the value is live.
17+
//
18+
// See also: [weak.Pointer].
19+
type Live[K comparable, V any] struct {
20+
create func(context.Context, K) (*V, error)
21+
m sync.Map
22+
sf singleflight.Group[K, *V]
23+
}
24+
25+
// NewLive ...
26+
//
27+
// If the "create" function needs a struct to be able to construct a value,
28+
// consider using [unique.Handle] to be able to satisfy the "comparable"
29+
// constraint.
30+
func NewLive[K comparable, V any](create func(context.Context, K) (*V, error)) *Live[K, V] {
31+
return &Live[K, V]{create: create}
32+
}
33+
34+
var _ unique.Handle[string] // for docs
35+
36+
// Get ...
37+
func (c *Live[K, V]) Get(ctx context.Context, key K) (*V, error) {
38+
for {
39+
// Try to load an existing value out of the cache.
40+
value, ok := c.m.Load(key)
41+
if !ok {
42+
// No value found. Create a new value.
43+
fn := func() (*V, error) {
44+
// Eagerly check the Context so that every create function
45+
// doesn't need the preamble.
46+
//
47+
// Do this because this goroutine may have gone around the loop
48+
// multiple times and found entries in the map that had
49+
// invalidated weak pointers, so the context may have exprired.
50+
if ctx.Err() != nil {
51+
return nil, context.Cause(ctx)
52+
}
53+
v, err := c.create(ctx, key)
54+
if err != nil {
55+
return nil, err
56+
}
57+
58+
wp := weak.Make(v)
59+
c.m.Store(key, wp)
60+
runtime.AddCleanup(v, func(key K) {
61+
// Only delete if the weak pointer is equal. If it's not,
62+
// someone else already deleted the entry and installed a
63+
// new pointer.
64+
c.m.CompareAndDelete(key, wp)
65+
}, key)
66+
return v, nil
67+
}
68+
69+
ch := c.sf.DoChan(key, fn)
70+
select {
71+
case res := <-ch:
72+
return res.Val, res.Err
73+
case <-ctx.Done():
74+
c.sf.Forget(key)
75+
return nil, context.Cause(ctx)
76+
}
77+
}
78+
79+
// See if our cache entry is valid.
80+
if v := value.(weak.Pointer[V]).Value(); v != nil {
81+
return v, nil
82+
}
83+
// Discovered a nil entry awaiting cleanup. Eagerly delete it.
84+
c.m.CompareAndDelete(key, value)
85+
}
86+
}

internal/cache/doc.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Package cache provides caching implementations for Go values.
2+
package cache

0 commit comments

Comments
 (0)