Skip to content

Commit 561ba93

Browse files
committed
Initial version
1 parent 139fafd commit 561ba93

File tree

9 files changed

+531
-23
lines changed

9 files changed

+531
-23
lines changed

‎.github/workflows/test.yml‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,15 @@ jobs:
3737
- name: Lint
3838
run: golint ./...
3939
- name: Test
40-
run: go test -race ./...
40+
run: go test -race ./... -coverpkg=./... -coverprofile=coverage.txt -covermode=atomic
41+
- name: Upload coverage
42+
if: success() && matrix.platform == 'ubuntu-latest'
43+
run: |
44+
curl https://keybase.io/codecovsecurity/pgp_keys.asc | gpg --no-default-keyring --keyring trustedkeys.gpg --import # One-time step
45+
curl -Os https://uploader.codecov.io/latest/linux/codecov
46+
curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM
47+
curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM.sig
48+
gpgv codecov.SHA256SUM.sig codecov.SHA256SUM
49+
shasum -a 256 -c codecov.SHA256SUM
50+
chmod +x codecov
51+
./codecov

‎README.md‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1-
[![Tests on Linux, MacOS and Windows](https://github.com/bep/golibtemplate/workflows/Test/badge.svg)](https://github.com/bep/golibtemplate/actions?query=workflow:Test)
2-
[![Go Report Card](https://goreportcard.com/badge/github.com/bep/golibtemplate)](https://goreportcard.com/report/github.com/bep/golibtemplate)
3-
[![GoDoc](https://godoc.org/github.com/bep/golibtemplate?status.svg)](https://godoc.org/github.com/bep/golibtemplate)
1+
[![Tests on Linux, MacOS and Windows](https://github.com/bep/lazycache/workflows/Test/badge.svg)](https://github.com/bep/lazycache/actions?query=workflow:Test)
2+
[![Go Report Card](https://goreportcard.com/badge/github.com/bep/lazycache)](https://goreportcard.com/report/github.com/bep/lazycache)
3+
[![codecov](https://codecov.io/github/bep/lazycache/branch/main/graph/badge.svg?token=HJCUCT07CH)](https://codecov.io/github/bep/lazycache)
4+
[![GoDoc](https://godoc.org/github.com/bep/lazycache?status.svg)](https://godoc.org/github.com/bep/lazycache)
5+
6+
**Note:** This is still a work in progress and the API will most likely change.
7+
8+
**Lazycache** is a simple thread safe in-memory LRU cache. Under the hood it leverages the great [simpleru package in golang-lru](https://github.com/hashicorp/golang-lru), with its exellent performance. One big difference between `golang-lru` and this library is the [GetOrCreate](https://pkg.go.dev/github.com/bep/lazycache#LazyCache.GetOrCreate) method, which provides:
9+
10+
* Non-blocking cache priming on cache misses.
11+
* A guarantee that the prime function is only called once for a given key.
12+
* The cache's [RWMutex](https://pkg.go.dev/sync#RWMutex) is not locked during the execution of the prime function, which should make it easier to reason about potential deadlocks.
13+
14+
Other notable features:
15+
16+
* The cache can be [resized](https://pkg.go.dev/github.com/bep/lazycache#LazyCache.Resize) while running.
17+
* When the number of entries overflows the defined cache size, the least recently used item gets discarded (LRU).
18+

‎codecov.yml‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
coverage:
2+
status:
3+
project:
4+
default:
5+
target: auto
6+
threshold: 0.5%
7+
patch: off
8+
9+
comment:
10+
require_changes: true

‎go.mod‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
module github.com/bep/golibtemplate
1+
module github.com/bep/lazycache
22

33
go 1.18
44

55
require (
6-
github.com/frankban/quicktest v1.14.2 // indirect
6+
github.com/frankban/quicktest v1.14.2
7+
github.com/hashicorp/golang-lru v0.5.4
8+
)
9+
10+
require (
711
github.com/google/go-cmp v0.5.7 // indirect
812
github.com/kr/pretty v0.3.0 // indirect
913
github.com/kr/text v0.2.0 // indirect

‎go.sum‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ github.com/frankban/quicktest v1.14.2 h1:SPb1KFFmM+ybpEjPUhCCkZOM5xlovT5UbrMvWnX
33
github.com/frankban/quicktest v1.14.2/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
44
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
55
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=
68
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
79
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
810
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=

‎lazycache.go‎

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
package lazycache
2+
3+
import (
4+
"sync"
5+
6+
"github.com/hashicorp/golang-lru/simplelru"
7+
)
8+
9+
var _ = Entry(&delayedEntry{})
10+
11+
// New creates a new LazyCache.
12+
func New(options CacheOptions) *LazyCache {
13+
lru, err := simplelru.NewLRU(int(options.MaxEntries), nil)
14+
if err != nil {
15+
panic(err)
16+
}
17+
c := &LazyCache{
18+
lru: lru,
19+
}
20+
return c
21+
}
22+
23+
type CacheOptions struct {
24+
// MaxEntries is the maximum number of entries that the cache should hold.
25+
// Note that this can also be adjusted after the cache is created with Resize.
26+
MaxEntries int
27+
}
28+
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 LazyCache struct {
37+
lru *simplelru.LRU
38+
mu sync.RWMutex
39+
}
40+
41+
// Contains returns true if the given key is in the cache.
42+
func (c *LazyCache) Contains(key any) bool {
43+
c.mu.RLock()
44+
b := c.lru.Contains(key)
45+
c.mu.RUnlock()
46+
return b
47+
}
48+
49+
// Delete deletes the item with given key from the cache, returning if the
50+
// key was contained.
51+
func (c *LazyCache) Delete(key any) bool {
52+
c.mu.Lock()
53+
defer c.mu.Unlock()
54+
return c.lru.Remove(key)
55+
}
56+
57+
// DeleteFunc deletes all entries for which the given function returns true.
58+
func (c *LazyCache) DeleteFunc(matches func(key any, item Entry) bool) int {
59+
c.mu.RLock()
60+
keys := c.lru.Keys()
61+
62+
var keysToDelete []any
63+
for _, key := range keys {
64+
v, _ := c.lru.Peek(key)
65+
if matches(key, v.(Entry)) {
66+
keysToDelete = append(keysToDelete, key)
67+
}
68+
}
69+
c.mu.RUnlock()
70+
71+
c.mu.Lock()
72+
defer c.mu.Unlock()
73+
var deleteCount int
74+
for _, key := range keysToDelete {
75+
if c.lru.Remove(key) {
76+
deleteCount++
77+
}
78+
}
79+
80+
return deleteCount
81+
}
82+
83+
// Keys returns a slice of the keys in the cache, oldest first.
84+
func (c *LazyCache) 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 *LazyCache) Len() int {
92+
c.mu.RLock()
93+
defer c.mu.RUnlock()
94+
return c.lru.Len()
95+
}
96+
97+
// Get returns the value associated with key.
98+
func (c *LazyCache) Get(key any) Entry {
99+
c.mu.Lock()
100+
v, ok := c.lru.Get(key)
101+
c.mu.Unlock()
102+
if !ok {
103+
return entry{}
104+
}
105+
return v.(Entry)
106+
}
107+
108+
// GetOrCreate returns the value associated with key, or creates it if it doesn't.
109+
// Note that create, the cache prime function, is called once and then not called again for a given key
110+
// unless the cache entry is evicted; it does not block other goroutines from calling GetOrCreate,
111+
// it is not called with the cache lock held.
112+
func (c *LazyCache) GetOrCreate(key any, create func(key any) (any, error)) Entry {
113+
c.mu.Lock()
114+
v, ok := c.lru.Get(key)
115+
if ok {
116+
c.mu.Unlock()
117+
return v.(Entry)
118+
}
119+
120+
var e = &delayedEntry{
121+
done: make(chan struct{}),
122+
}
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)
126+
c.mu.Unlock()
127+
128+
// Create the value with the lock released.
129+
v, err := create(key)
130+
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)
136+
137+
return e
138+
}
139+
140+
// Resize changes the cache size and returns the number of entries evicted.
141+
func (c *LazyCache) Resize(size int) (evicted int) {
142+
c.mu.Lock()
143+
evicted = c.lru.Resize(size)
144+
c.mu.Unlock()
145+
return evicted
146+
}
147+
148+
// Set associates value with key.
149+
func (c *LazyCache) Set(key, value any) {
150+
c.mu.Lock()
151+
if _, ok := value.(Entry); !ok {
152+
value = entry{
153+
value: value,
154+
}
155+
}
156+
c.lru.Add(key, value)
157+
c.mu.Unlock()
158+
}
159+
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
165+
}
166+
167+
func (r *delayedEntry) Value() any {
168+
<-r.done
169+
return r.value
170+
}
171+
172+
func (r *delayedEntry) Err() error {
173+
<-r.done
174+
return r.err
175+
}
176+
177+
type entry struct {
178+
value any
179+
err error
180+
}
181+
182+
func (r entry) Value() any {
183+
return r.value
184+
}
185+
186+
func (r entry) Err() error {
187+
return r.err
188+
}

0 commit comments

Comments
 (0)