Skip to content

Commit e071df4

Browse files
committed
first lfu cache commit
1 parent 61d859c commit e071df4

File tree

5 files changed

+298
-8
lines changed

5 files changed

+298
-8
lines changed

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

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: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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+
e := newEntry(key, val, opts...)
62+
heap.Push(c.queue, e)
63+
c.items[key] = e
64+
65+
if len(c.items) > c.cap {
66+
evictedEntry := heap.Pop(c.queue).(*entry[K, V])
67+
delete(c.items, evictedEntry.item.Key)
68+
}
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+
}

lfu/priority_queue_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package lfu
2+
3+
import (
4+
"container/heap"
5+
"testing"
6+
"time"
7+
)
8+
9+
func TestPriorityQueue(t *testing.T) {
10+
// perl -MList::Util -e 'print join ",", List::Util::shuffle(1..10)'
11+
nums := []int{2, 1, 4, 5, 6, 9, 7, 10, 8, 3}
12+
queue := newPriorityQueue[int, int](len(nums))
13+
entries := make([]*entry[int, int], 0, len(nums))
14+
15+
for _, v := range nums {
16+
entry := newEntry(v, v)
17+
entries = append(entries, entry)
18+
heap.Push(queue, entry)
19+
}
20+
21+
if got := queue.Len(); len(nums) != got {
22+
t.Errorf("want %d, but got %d", len(nums), got)
23+
}
24+
25+
// check the initial state
26+
for idx, entry := range *queue {
27+
if entry.index != idx {
28+
t.Errorf("want index %d, but got %d", entry.index, idx)
29+
}
30+
if entry.item.ReferenceCount != 1 {
31+
t.Errorf("want count 1")
32+
}
33+
if got := entry.item.Value; nums[idx] != got {
34+
t.Errorf("want value %d but got %d", nums[idx], got)
35+
}
36+
}
37+
38+
// updates len - 1 entries (updated all reference count and referenced_at)
39+
// so the lowest priority will be the last element.
40+
//
41+
// this loop creates
42+
// - Reference counters other than the last element are 2.
43+
// - The first element is the oldest referenced_at in reference counter is 2
44+
for i := 0; i < len(nums)-1; i++ {
45+
entry := entries[i]
46+
queue.update(entry, nums[i])
47+
time.Sleep(time.Millisecond)
48+
}
49+
50+
// check the priority by reference counter
51+
wantValue := nums[len(nums)-1]
52+
got := heap.Pop(queue).(*entry[int, int])
53+
if got.index != -1 {
54+
t.Errorf("want index -1, but got %d", got.index)
55+
}
56+
if wantValue != got.item.Value {
57+
t.Errorf("want the lowest priority value is %d, but got %d", wantValue, got.item.Value)
58+
}
59+
if want, got := len(nums)-1, queue.Len(); want != got {
60+
t.Errorf("want %d, but got %d", want, got)
61+
}
62+
63+
// check the priority by referenced_at
64+
wantValue2 := nums[0]
65+
got2 := heap.Pop(queue).(*entry[int, int])
66+
if got.index != -1 {
67+
t.Errorf("want index -1, but got %d", got.index)
68+
}
69+
if wantValue2 != got2.item.Value {
70+
t.Errorf("want the lowest priority value is %d, but got %d", wantValue2, got2.item.Value)
71+
}
72+
if want, got := len(nums)-2, queue.Len(); want != got {
73+
t.Errorf("want %d, but got %d", want, got)
74+
}
75+
}

lfu/priotiry_queue.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package lfu
2+
3+
import (
4+
"container/heap"
5+
6+
cache "github.com/Code-Hex/go-generics-cache"
7+
)
8+
9+
type entry[K comparable, V any] struct {
10+
index int
11+
item *cache.Item[K, V]
12+
}
13+
14+
func newEntry[K comparable, V any](key K, val V, opts ...cache.ItemOption) *entry[K, V] {
15+
return &entry[K, V]{
16+
index: 0,
17+
item: cache.NewItem(key, val, opts...),
18+
}
19+
}
20+
21+
type priorityQueue[K comparable, V any] []*entry[K, V]
22+
23+
func newPriorityQueue[K comparable, V any](cap int) *priorityQueue[K, V] {
24+
queue := make(priorityQueue[K, V], 0, cap)
25+
return &queue
26+
}
27+
28+
// see example of priority queue: https://pkg.go.dev/container/heap
29+
var _ heap.Interface = (*priorityQueue[interface{}, interface{}])(nil)
30+
31+
func (l priorityQueue[K, V]) Len() int { return len(l) }
32+
33+
func (l priorityQueue[K, V]) Less(i, j int) bool {
34+
if l[i].item.ReferenceCount == l[j].item.ReferenceCount {
35+
return l[i].item.ReferencedAt.Before(l[j].item.ReferencedAt)
36+
}
37+
return l[i].item.ReferenceCount < l[j].item.ReferenceCount
38+
}
39+
40+
func (l priorityQueue[K, V]) Swap(i, j int) {
41+
l[i], l[j] = l[j], l[i]
42+
l[i].index = i
43+
l[i].index = j
44+
}
45+
46+
func (l *priorityQueue[K, V]) Push(x interface{}) {
47+
entry := x.(*entry[K, V])
48+
entry.index = len(*l)
49+
*l = append(*l, entry)
50+
}
51+
52+
func (l *priorityQueue[K, V]) Pop() interface{} {
53+
old := *l
54+
n := len(old)
55+
entry := old[n-1]
56+
old[n-1] = nil // avoid memory leak
57+
entry.index = -1 // for safety
58+
*l = old[0 : n-1]
59+
return entry
60+
}
61+
62+
func (pq *priorityQueue[K, V]) update(e *entry[K, V], val V) {
63+
e.item.Value = val
64+
e.item.Referenced()
65+
heap.Fix(pq, e.index)
66+
}

0 commit comments

Comments
 (0)