Skip to content

Commit 82f85aa

Browse files
authored
Merge pull request #7 from Code-Hex/add/lfu
added lfu cache
2 parents 61d859c + 227fa9b commit 82f85aa

File tree

9 files changed

+470
-8
lines changed

9 files changed

+470
-8
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ go-generics-cache is an in-memory key:value store/cache that is suitable for app
1010
- See [examples](https://github.com/Code-Hex/go-generics-cache/blob/main/simple/example_test.go)
1111
- LRU cache
1212
- See [examples](https://github.com/Code-Hex/go-generics-cache/blob/main/lru/example_test.go)
13+
- LFU cache
14+
- See [examples](https://github.com/Code-Hex/go-generics-cache/blob/main/lfu/example_test.go)
15+
- [An O(1) algorithm for implementing the LFU cache eviction scheme](http://dhruvbird.com/lfu.pdf)
1316

1417
## Requirements
1518

cache.go

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,17 @@ type Cache[K comparable, V any] interface {
1616

1717
// Item is an item
1818
type Item[K comparable, V any] struct {
19-
Key K
20-
Value V
21-
Expiration time.Duration
22-
CreatedAt time.Time
19+
Key K
20+
Value V
21+
ReferenceCount int
22+
Expiration time.Duration
23+
CreatedAt time.Time
24+
ReferencedAt time.Time
25+
}
26+
27+
func (i *Item[K, V]) Referenced() {
28+
i.ReferenceCount++
29+
i.ReferencedAt = nowFunc()
2330
}
2431

2532
var nowFunc = time.Now
@@ -53,11 +60,14 @@ func NewItem[K comparable, V any](key K, val V, opts ...ItemOption) *Item[K, V]
5360
for _, optFunc := range opts {
5461
optFunc(o)
5562
}
63+
now := nowFunc()
5664
return &Item[K, V]{
57-
Key: key,
58-
Value: val,
59-
Expiration: o.expiration,
60-
CreatedAt: nowFunc(),
65+
Key: key,
66+
Value: val,
67+
ReferenceCount: 1,
68+
Expiration: o.expiration,
69+
CreatedAt: now,
70+
ReferencedAt: now,
6171
}
6272
}
6373

cache_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,31 @@ func TestHasExpired(t *testing.T) {
101101
})
102102
}
103103
}
104+
105+
func TestReferenced(t *testing.T) {
106+
tm := time.Now()
107+
reset := cache.SetNowFunc(tm)
108+
109+
item := cache.NewItem("hello", "world")
110+
if item.ReferenceCount != 1 {
111+
t.Errorf("want ref count is 1 but got %d", item.ReferenceCount)
112+
}
113+
if !item.ReferencedAt.Equal(tm) {
114+
t.Errorf("unexpected referenced_at got %v", item.ReferencedAt)
115+
}
116+
117+
reset()
118+
// update referenced_at
119+
wantReferencedAt := tm.Add(time.Hour)
120+
reset = cache.SetNowFunc(wantReferencedAt)
121+
defer reset()
122+
123+
item.Referenced()
124+
125+
if item.ReferenceCount != 2 {
126+
t.Errorf("want ref count is 2 but got %d", item.ReferenceCount)
127+
}
128+
if !item.ReferencedAt.Equal(wantReferencedAt) {
129+
t.Errorf("unexpected referenced_at got %v", item.ReferencedAt)
130+
}
131+
}

lfu/example_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package lfu_test
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/Code-Hex/go-generics-cache/lfu"
7+
)
8+
9+
func ExampleLFUCache() {
10+
c := lfu.NewCache[string, int]()
11+
c.Set("a", 1)
12+
c.Set("b", 2)
13+
av, aok := c.Get("a")
14+
bv, bok := c.Get("b")
15+
cv, cok := c.Get("c")
16+
fmt.Println(av, aok)
17+
fmt.Println(bv, bok)
18+
fmt.Println(cv, cok)
19+
// Output:
20+
// 1 true
21+
// 2 true
22+
// 0 false
23+
}
24+
25+
func ExampleCacheKeys() {
26+
c := lfu.NewCache[string, int]()
27+
c.Set("a", 1)
28+
c.Set("b", 2)
29+
c.Set("c", 3)
30+
keys := c.Keys()
31+
for _, key := range keys {
32+
fmt.Println(key)
33+
}
34+
// Output:
35+
// a
36+
// b
37+
// c
38+
}

lfu/lfu.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package lfu
2+
3+
import (
4+
"container/heap"
5+
"sync"
6+
7+
cache "github.com/Code-Hex/go-generics-cache"
8+
)
9+
10+
// Cache is a thread safe LRU cache
11+
type Cache[K comparable, V any] struct {
12+
cap int
13+
queue *priorityQueue[K, V]
14+
items map[K]*entry[K, V]
15+
mu sync.RWMutex
16+
}
17+
18+
var _ cache.Cache[interface{}, any] = (*Cache[interface{}, any])(nil)
19+
20+
// NewCache creates a new LFU cache whose capacity is the default size (128).
21+
func NewCache[K comparable, V any]() *Cache[K, V] {
22+
return NewCacheWithCap[K, V](128)
23+
}
24+
25+
// NewCacheWithCap creates a new LFU cache whose capacity is the specified size.
26+
func NewCacheWithCap[K comparable, V any](cap int) *Cache[K, V] {
27+
return &Cache[K, V]{
28+
cap: cap,
29+
queue: newPriorityQueue[K, V](cap),
30+
items: make(map[K]*entry[K, V], cap),
31+
}
32+
}
33+
34+
// Get looks up a key's value from the cache.
35+
func (c *Cache[K, V]) Get(key K) (zero V, _ bool) {
36+
c.mu.RLock()
37+
defer c.mu.RUnlock()
38+
39+
e, ok := c.items[key]
40+
if !ok {
41+
return
42+
}
43+
if e.item.HasExpired() {
44+
return
45+
}
46+
e.item.Referenced()
47+
heap.Fix(c.queue, e.index)
48+
return e.item.Value, true
49+
}
50+
51+
// Set sets a value to the cache with key. replacing any existing value.
52+
func (c *Cache[K, V]) Set(key K, val V, opts ...cache.ItemOption) {
53+
c.mu.Lock()
54+
defer c.mu.Unlock()
55+
56+
if e, ok := c.items[key]; ok {
57+
c.queue.update(e, val)
58+
return
59+
}
60+
61+
if len(c.items) == c.cap {
62+
evictedEntry := heap.Pop(c.queue).(*entry[K, V])
63+
delete(c.items, evictedEntry.item.Key)
64+
}
65+
66+
e := newEntry(key, val, opts...)
67+
heap.Push(c.queue, e)
68+
c.items[key] = e
69+
}
70+
71+
// Keys returns the keys of the cache. the order is from oldest to newest.
72+
func (c *Cache[K, V]) Keys() []K {
73+
c.mu.RLock()
74+
defer c.mu.RUnlock()
75+
keys := make([]K, 0, len(c.items))
76+
for _, entry := range *c.queue {
77+
keys = append(keys, entry.item.Key)
78+
}
79+
return keys
80+
}
81+
82+
// Delete deletes the item with provided key from the cache.
83+
func (c *Cache[K, V]) Delete(key K) {
84+
c.mu.Lock()
85+
defer c.mu.Unlock()
86+
if e, ok := c.items[key]; ok {
87+
heap.Remove(c.queue, e.index)
88+
delete(c.items, key)
89+
}
90+
}
91+
92+
// Contains reports whether key is within cache.
93+
func (c *Cache[K, V]) Contains(key K) bool {
94+
c.mu.RLock()
95+
defer c.mu.RUnlock()
96+
e, ok := c.items[key]
97+
if !ok {
98+
return false
99+
}
100+
return !e.item.HasExpired()
101+
}
102+
103+
// Len returns the number of items in the cache.
104+
func (c *Cache[K, V]) Len() int {
105+
c.mu.RLock()
106+
defer c.mu.RUnlock()
107+
return c.queue.Len()
108+
}

lfu/lfu_internal_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package lfu
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
cache "github.com/Code-Hex/go-generics-cache"
8+
)
9+
10+
func TestContains(t *testing.T) {
11+
t.Run("without expiration", func(t *testing.T) {
12+
cache := NewCache[string, int]()
13+
cache.Set("foo", 1)
14+
cache.Set("bar", 2)
15+
cache.Set("baz", 3)
16+
for _, key := range []string{
17+
"foo",
18+
"bar",
19+
"baz",
20+
} {
21+
if !cache.Contains(key) {
22+
t.Errorf("not found: %s", key)
23+
}
24+
}
25+
if cache.Contains("not found") {
26+
t.Errorf("found")
27+
}
28+
})
29+
30+
t.Run("with expiration", func(t *testing.T) {
31+
c := NewCache[string, int]()
32+
key := "foo"
33+
exp := time.Hour
34+
c.Set(key, 1, cache.WithExpiration(exp))
35+
// modify directly
36+
e, ok := c.items[key]
37+
if !ok {
38+
t.Fatal("unexpected not found key")
39+
}
40+
item := e.item
41+
item.CreatedAt = time.Now().Add(-2 * exp)
42+
43+
if c.Contains(key) {
44+
t.Errorf("found")
45+
}
46+
})
47+
}
48+
49+
func TestGet(t *testing.T) {
50+
c := NewCache[string, int]()
51+
key := "foo"
52+
exp := time.Hour
53+
c.Set(key, 1, cache.WithExpiration(exp))
54+
_, ok := c.Get(key)
55+
if !ok {
56+
t.Fatal("unexpected not found")
57+
}
58+
// modify directly
59+
e, ok := c.items[key]
60+
if !ok {
61+
t.Fatal("unexpected not found key")
62+
}
63+
item := e.item
64+
item.CreatedAt = time.Now().Add(-2 * exp)
65+
_, ok2 := c.Get(key)
66+
if ok2 {
67+
t.Fatal("unexpected found (expired)")
68+
}
69+
}

lfu/lfu_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package lfu_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/Code-Hex/go-generics-cache/lfu"
7+
)
8+
9+
func TestSet(t *testing.T) {
10+
// set capacity is 1
11+
cache := lfu.NewCacheWithCap[string, int](1)
12+
cache.Set("foo", 1)
13+
if got := cache.Len(); got != 1 {
14+
t.Fatalf("invalid length: %d", got)
15+
}
16+
if got, ok := cache.Get("foo"); got != 1 || !ok {
17+
t.Fatalf("invalid value got %d, cachehit %v", got, ok)
18+
}
19+
20+
// if over the cap
21+
cache.Set("bar", 2)
22+
if got := cache.Len(); got != 1 {
23+
t.Fatalf("invalid length: %d", got)
24+
}
25+
bar, ok := cache.Get("bar")
26+
if bar != 2 || !ok {
27+
t.Fatalf("invalid value bar %d, cachehit %v", bar, ok)
28+
}
29+
30+
// checks deleted oldest
31+
if _, ok := cache.Get("foo"); ok {
32+
t.Fatalf("invalid delete oldest value foo %v", ok)
33+
}
34+
35+
// valid: if over the cap but same key
36+
cache.Set("bar", 100)
37+
if got := cache.Len(); got != 1 {
38+
t.Fatalf("invalid length: %d", got)
39+
}
40+
bar, ok = cache.Get("bar")
41+
if bar != 100 || !ok {
42+
t.Fatalf("invalid replacing value bar %d, cachehit %v", bar, ok)
43+
}
44+
}
45+
46+
func TestDelete(t *testing.T) {
47+
cache := lfu.NewCacheWithCap[string, int](1)
48+
cache.Set("foo", 1)
49+
if got := cache.Len(); got != 1 {
50+
t.Fatalf("invalid length: %d", got)
51+
}
52+
53+
cache.Delete("foo2")
54+
if got := cache.Len(); got != 1 {
55+
t.Fatalf("invalid length after deleted does not exist key: %d", got)
56+
}
57+
58+
cache.Delete("foo")
59+
if got := cache.Len(); got != 0 {
60+
t.Fatalf("invalid length after deleted: %d", got)
61+
}
62+
if _, ok := cache.Get("foo"); ok {
63+
t.Fatalf("invalid get after deleted %v", ok)
64+
}
65+
}

0 commit comments

Comments
 (0)