Skip to content

Commit e380173

Browse files
committed
Make it generic
Which makes it more pleasant to use and, in general, faster: ```bash name old time/op new time/op delta GetOrCreate/Real-4 276µs ±82% 351µs ±43% ~ (p=0.343 n=4+4) CacheSerial/Set-4 176ns ± 0% 120ns ± 0% -31.96% (p=0.029 n=4+4) CacheSerial/Get-4 27.1ns ± 2% 20.7ns ± 1% -23.62% (p=0.029 n=4+4) CacheParallel/Set-4 272ns ± 1% 233ns ± 0% -14.05% (p=0.029 n=4+4) CacheParallel/Get-4 149ns ± 1% 144ns ± 2% -3.33% (p=0.029 n=4+4) name old alloc/op new alloc/op delta GetOrCreate/Real-4 37.5B ± 9% 26.5B ± 9% -29.33% (p=0.029 n=4+4) CacheSerial/Set-4 128B ± 0% 104B ± 0% -18.75% (p=0.029 n=4+4) CacheSerial/Get-4 0.00B 0.00B ~ (all equal) CacheParallel/Set-4 128B ± 0% 104B ± 0% -18.75% (p=0.029 n=4+4) CacheParallel/Get-4 0.00B 0.00B ~ (all equal) name old allocs/op new allocs/op delta GetOrCreate/Real-4 1.50 ±33% 1.00 ± 0% ~ (p=0.429 n=4+4) CacheSerial/Set-4 4.00 ± 0% 2.00 ± 0% -50.00% (p=0.029 n=4+4) CacheSerial/Get-4 0.00 0.00 ~ (all equal) CacheParallel/Set-4 4.00 ± 0% 2.00 ± 0% -50.00% (p=0.029 n=4+4) CacheParallel/Get-4 0.00 0.00 ~ (all equal) ``` Note that this uses a temporary fork of hashicorp/golang-lru#111 ... which, cross fingers, will get merged soon.
1 parent 9e57b69 commit e380173

File tree

4 files changed

+221
-207
lines changed

4 files changed

+221
-207
lines changed

‎go.mod‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ module github.com/bep/lazycache
33
go 1.18
44

55
require (
6+
github.com/bep/golang-lru/v2 v2.0.0-20221109181639-7772c2d8d424
67
github.com/frankban/quicktest v1.14.2
7-
github.com/hashicorp/golang-lru v0.5.4
88
)
99

1010
require (

‎go.sum‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1+
github.com/bep/golang-lru/v2 v2.0.0-20221109181639-7772c2d8d424 h1:pCl5RyZx5P/AEKfWw0pNOLdDssKywh7O5i1vOWMYk6k=
2+
github.com/bep/golang-lru/v2 v2.0.0-20221109181639-7772c2d8d424/go.mod h1:VLsj4zCnENLusS8ylWozfleW5d8IN49A36vXR5PVoh4=
13
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
24
github.com/frankban/quicktest v1.14.2 h1:SPb1KFFmM+ybpEjPUhCCkZOM5xlovT5UbrMvWnXyBns=
35
github.com/frankban/quicktest v1.14.2/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
46
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
57
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
6-
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
7-
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
88
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
99
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
1010
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=

‎lazycache.go‎

Lines changed: 88 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -3,66 +3,56 @@ package lazycache
33
import (
44
"sync"
55

6-
"github.com/hashicorp/golang-lru/simplelru"
6+
"github.com/bep/golang-lru/v2/simplelru"
77
)
88

9-
var _ = Entry(&delayedEntry{})
10-
119
// New creates a new Cache.
12-
func New(options CacheOptions) *Cache {
13-
lru, err := simplelru.NewLRU(int(options.MaxEntries), nil)
10+
func New[K comparable, V any](options Options) *Cache[K, V] {
11+
lru, err := simplelru.NewLRU[K, *valueWrapper[V]](int(options.MaxEntries), nil)
1412
if err != nil {
1513
panic(err)
1614
}
17-
c := &Cache{
15+
c := &Cache[K, V]{
1816
lru: lru,
1917
}
2018
return c
2119
}
2220

23-
type CacheOptions struct {
21+
// Options holds the cache options.
22+
type Options struct {
2423
// MaxEntries is the maximum number of entries that the cache should hold.
2524
// Note that this can also be adjusted after the cache is created with Resize.
2625
MaxEntries int
2726
}
2827

29-
// Entry is the result of a cache lookup.
30-
// Any Err value is the error that was returned by the cache prime function. This error value is cached. TODO(bep) consider this.
31-
type Entry interface {
32-
Value() any
33-
Err() error
34-
}
35-
36-
type Cache struct {
37-
lru *simplelru.LRU
28+
// Cache is a thread-safe resizable LRU cache.
29+
type Cache[K comparable, V any] struct {
30+
lru *simplelru.LRU[K, *valueWrapper[V]]
3831
mu sync.RWMutex
39-
}
4032

41-
// Contains returns true if the given key is in the cache.
42-
func (c *Cache) Contains(key any) bool {
43-
c.mu.RLock()
44-
b := c.lru.Contains(key)
45-
c.mu.RUnlock()
46-
return b
33+
zerov V
4734
}
4835

4936
// Delete deletes the item with given key from the cache, returning if the
5037
// key was contained.
51-
func (c *Cache) Delete(key any) bool {
38+
func (c *Cache[K, V]) Delete(key K) bool {
5239
c.mu.Lock()
5340
defer c.mu.Unlock()
5441
return c.lru.Remove(key)
5542
}
5643

5744
// DeleteFunc deletes all entries for which the given function returns true.
58-
func (c *Cache) DeleteFunc(matches func(key any, item Entry) bool) int {
45+
func (c *Cache[K, V]) DeleteFunc(matches func(key K, item V) bool) int {
5946
c.mu.RLock()
6047
keys := c.lru.Keys()
6148

62-
var keysToDelete []any
49+
var keysToDelete []K
6350
for _, key := range keys {
64-
v, _ := c.lru.Peek(key)
65-
if matches(key, v.(Entry)) {
51+
w, _ := c.lru.Peek(key)
52+
if !w.wait().found {
53+
continue
54+
}
55+
if matches(key, w.value) {
6656
keysToDelete = append(keysToDelete, key)
6757
}
6858
}
@@ -80,109 +70,118 @@ func (c *Cache) DeleteFunc(matches func(key any, item Entry) bool) int {
8070
return deleteCount
8171
}
8272

83-
// Keys returns a slice of the keys in the cache, oldest first.
84-
func (c *Cache) Keys() []any {
85-
c.mu.RLock()
86-
defer c.mu.RUnlock()
87-
return c.lru.Keys()
88-
}
89-
90-
// Len returns the number of items in the cache.
91-
func (c *Cache) Len() int {
92-
c.mu.RLock()
93-
defer c.mu.RUnlock()
94-
return c.lru.Len()
95-
}
96-
9773
// Get returns the value associated with key.
98-
func (c *Cache) Get(key any) Entry {
74+
func (c *Cache[K, V]) Get(key K) (V, bool) {
9975
c.mu.Lock()
100-
v, ok := c.lru.Get(key)
76+
w := c.get(key)
10177
c.mu.Unlock()
102-
if !ok {
103-
return entry{}
78+
if w == nil {
79+
return c.zerov, false
10480
}
105-
return v.(Entry)
81+
w.wait()
82+
return w.value, w.found
10683
}
10784

10885
// GetOrCreate returns the value associated with key, or creates it if it doesn't.
10986
// Note that create, the cache prime function, is called once and then not called again for a given key
11087
// unless the cache entry is evicted; it does not block other goroutines from calling GetOrCreate,
11188
// it is not called with the cache lock held.
112-
func (c *Cache) GetOrCreate(key any, create func(key any) (any, error)) Entry {
89+
func (c *Cache[K, V]) GetOrCreate(key K, create func(key K) (V, error)) (V, bool, error) {
11390
c.mu.Lock()
114-
v, ok := c.lru.Get(key)
115-
if ok {
116-
c.mu.Unlock()
117-
return v.(Entry)
91+
w := c.get(key)
92+
if w != nil {
93+
w.wait()
94+
// if w.ready is set then w comes from a concurrent GetOrCreate call.
95+
if w.found || w.ready != nil {
96+
c.mu.Unlock()
97+
return w.value, w.found, nil
98+
}
11899
}
119100

120-
var e = &delayedEntry{
121-
done: make(chan struct{}),
101+
w = &valueWrapper[V]{
102+
ready: make(chan struct{}),
122103
}
123-
// Add the *delayedEntry early and release the lock.
124-
// Calllers coming in getting the same cache entry will block on the done channel.
125-
c.lru.Add(key, e)
104+
105+
// Concurrent access to the same key will see w, but needs to wait for w.ready
106+
// to get the value.
107+
c.lru.Add(key, w)
126108
c.mu.Unlock()
127109

128110
// Create the value with the lock released.
129111
v, err := create(key)
112+
w.err = err
113+
w.value = v
114+
w.found = err == nil
130115

131-
// e is a pointer, and these values will be available to other callers getting this cache entry,
132-
// once the done channel is closed.
133-
e.err = err
134-
e.value = v
135-
close(e.done)
116+
close(w.ready)
136117

137-
return e
118+
if err != nil {
119+
c.Delete(key)
120+
return c.zerov, false, err
121+
}
122+
return v, true, nil
138123
}
139124

140125
// Resize changes the cache size and returns the number of entries evicted.
141-
func (c *Cache) Resize(size int) (evicted int) {
126+
func (c *Cache[K, V]) Resize(size int) (evicted int) {
142127
c.mu.Lock()
143128
evicted = c.lru.Resize(size)
144129
c.mu.Unlock()
145130
return evicted
146131
}
147132

148133
// Set associates value with key.
149-
func (c *Cache) Set(key, value any) {
134+
func (c *Cache[K, V]) Set(key K, value V) {
150135
c.mu.Lock()
151-
if _, ok := value.(Entry); !ok {
152-
value = entry{
153-
value: value,
154-
}
155-
}
156-
c.lru.Add(key, value)
136+
c.lru.Add(key, &valueWrapper[V]{value: value, found: true})
157137
c.mu.Unlock()
158138
}
159139

160-
// delayedEntry holds a cache value or error that is not available until the done channel is closed.
161-
type delayedEntry struct {
162-
done chan struct{}
163-
value any
164-
err error
140+
func (c *Cache[K, V]) get(key K) *valueWrapper[V] {
141+
w, ok := c.lru.Get(key)
142+
if !ok {
143+
return nil
144+
}
145+
return w
165146
}
166147

167-
func (r *delayedEntry) Value() any {
168-
<-r.done
169-
return r.value
148+
// contains returns true if the given key is in the cache.
149+
// note that this wil also return true if the key is in the cache but the value is not yet ready.
150+
func (c *Cache[K, V]) contains(key K) bool {
151+
c.mu.RLock()
152+
b := c.lru.Contains(key)
153+
c.mu.RUnlock()
154+
return b
170155
}
171156

172-
func (r *delayedEntry) Err() error {
173-
<-r.done
174-
return r.err
157+
// keys returns a slice of the keys in the cache, oldest first.
158+
// note that this wil also include keys that are not yet ready.
159+
func (c *Cache[K, V]) keys() []K {
160+
c.mu.RLock()
161+
defer c.mu.RUnlock()
162+
return c.lru.Keys()
175163
}
176164

177-
type entry struct {
178-
value any
179-
err error
165+
// len returns the number of items in the cache.
166+
// note that this wil also include values that are not yet ready.
167+
func (c *Cache[K, V]) len() int {
168+
c.mu.RLock()
169+
defer c.mu.RUnlock()
170+
return c.lru.Len()
180171
}
181172

182-
func (r entry) Value() any {
183-
return r.value
173+
// valueWrapper holds a cache value that is not available unless the done channel is nil or closed.
174+
// This construct makes more sense if you look at the code in GetOrCreate.
175+
type valueWrapper[V any] struct {
176+
value V
177+
found bool
178+
err error
179+
ready chan struct{}
184180
}
185181

186-
func (r entry) Err() error {
187-
return r.err
182+
func (w *valueWrapper[V]) wait() *valueWrapper[V] {
183+
if w.ready != nil {
184+
<-w.ready
185+
}
186+
return w
188187
}

0 commit comments

Comments
 (0)