Skip to content

Commit c2ea301

Browse files
Refactor cache implementations (#3239)
1 parent ee2e32a commit c2ea301

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+348
-572
lines changed

cache/cache.go

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,3 @@ type Cacher[K comparable, V any] interface {
2525
// Returns fraction of cache currently filled (0 --> 1)
2626
PortionFilled() float64
2727
}
28-
29-
// Evictable allows the object to be notified when it is evicted
30-
type Evictable[K comparable] interface {
31-
Key() K
32-
Evict()
33-
}
34-
35-
// Deduplicator acts as a best effort deduplication service
36-
type Deduplicator[K comparable, V Evictable[K]] interface {
37-
// Deduplicate returns either the provided value, or a previously provided
38-
// value with the same ID that hasn't yet been evicted
39-
Deduplicate(V) V
40-
41-
// Flush removes all entries from the cache
42-
Flush()
43-
}

cache/cachetest/cacher.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ var Tests = []struct {
2323
Size int
2424
Func func(t *testing.T, c cache.Cacher[ids.ID, int64])
2525
}{
26-
{Size: 1, Func: TestBasic},
27-
{Size: 2, Func: TestEviction},
26+
{Size: 1, Func: Basic},
27+
{Size: 2, Func: Eviction},
2828
}
2929

30-
func TestBasic(t *testing.T, cache cache.Cacher[ids.ID, int64]) {
30+
func Basic(t *testing.T, cache cache.Cacher[ids.ID, int64]) {
3131
require := require.New(t)
3232

3333
id1 := ids.ID{1}
@@ -62,7 +62,7 @@ func TestBasic(t *testing.T, cache cache.Cacher[ids.ID, int64]) {
6262
require.Equal(expectedValue2, value)
6363
}
6464

65-
func TestEviction(t *testing.T, cache cache.Cacher[ids.ID, int64]) {
65+
func Eviction(t *testing.T, cache cache.Cacher[ids.ID, int64]) {
6666
require := require.New(t)
6767

6868
id1 := ids.ID{1}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import "github.com/ava-labs/avalanchego/utils"
77

88
var _ Cacher[struct{}, struct{}] = (*Empty[struct{}, struct{}])(nil)
99

10+
// Empty is a cache that doesn't store anything.
1011
type Empty[K any, V any] struct{}
1112

1213
func (*Empty[K, V]) Put(K, V) {}

cache/lru/cache.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
package lru
5+
6+
import (
7+
"sync"
8+
9+
"github.com/ava-labs/avalanchego/cache"
10+
"github.com/ava-labs/avalanchego/utils"
11+
"github.com/ava-labs/avalanchego/utils/linked"
12+
)
13+
14+
var _ cache.Cacher[struct{}, struct{}] = (*Cache[struct{}, struct{}])(nil)
15+
16+
// Cache is a key value store with bounded size. If the size is attempted to be
17+
// exceeded, then an element is removed from the cache before the insertion is
18+
// done, based on evicting the least recently used value.
19+
type Cache[K comparable, V any] struct {
20+
lock sync.Mutex
21+
elements *linked.Hashmap[K, V]
22+
size int
23+
}
24+
25+
func NewCache[K comparable, V any](size int) *Cache[K, V] {
26+
return &Cache[K, V]{
27+
elements: linked.NewHashmap[K, V](),
28+
size: max(size, 1),
29+
}
30+
}
31+
32+
func (c *Cache[K, V]) Put(key K, value V) {
33+
c.lock.Lock()
34+
defer c.lock.Unlock()
35+
36+
if c.elements.Len() == c.size {
37+
oldestKey, _, _ := c.elements.Oldest()
38+
c.elements.Delete(oldestKey)
39+
}
40+
c.elements.Put(key, value)
41+
}
42+
43+
func (c *Cache[K, V]) Get(key K) (V, bool) {
44+
c.lock.Lock()
45+
defer c.lock.Unlock()
46+
47+
val, ok := c.elements.Get(key)
48+
if !ok {
49+
return utils.Zero[V](), false
50+
}
51+
c.elements.Put(key, val) // Mark [k] as MRU.
52+
return val, true
53+
}
54+
55+
func (c *Cache[K, _]) Evict(key K) {
56+
c.lock.Lock()
57+
defer c.lock.Unlock()
58+
59+
c.elements.Delete(key)
60+
}
61+
62+
func (c *Cache[_, _]) Flush() {
63+
c.lock.Lock()
64+
defer c.lock.Unlock()
65+
66+
c.elements.Clear()
67+
}
68+
69+
func (c *Cache[_, _]) Len() int {
70+
c.lock.Lock()
71+
defer c.lock.Unlock()
72+
73+
return c.elements.Len()
74+
}
75+
76+
func (c *Cache[_, _]) PortionFilled() float64 {
77+
c.lock.Lock()
78+
defer c.lock.Unlock()
79+
80+
return float64(c.elements.Len()) / float64(c.size)
81+
}

cache/lru/cache_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
package lru
5+
6+
import (
7+
"testing"
8+
9+
"github.com/ava-labs/avalanchego/cache/cachetest"
10+
"github.com/ava-labs/avalanchego/ids"
11+
)
12+
13+
func TestCache(t *testing.T) {
14+
c := NewCache[ids.ID, int64](1)
15+
cachetest.Basic(t, c)
16+
}
17+
18+
func TestCacheEviction(t *testing.T) {
19+
c := NewCache[ids.ID, int64](2)
20+
cachetest.Eviction(t, c)
21+
}

cache/lru/deduplicator.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
package lru
5+
6+
import (
7+
"sync"
8+
9+
"github.com/ava-labs/avalanchego/utils/linked"
10+
)
11+
12+
// Evictable allows the object to be notified when it is evicted
13+
//
14+
// Deprecated: Remove this once the vertex state no longer uses it.
15+
type Evictable[K comparable] interface {
16+
Key() K
17+
Evict()
18+
}
19+
20+
// Deduplicator is an LRU cache that notifies the objects when they are evicted.
21+
//
22+
// Deprecated: Remove this once the vertex state no longer uses it.
23+
type Deduplicator[K comparable, V Evictable[K]] struct {
24+
lock sync.Mutex
25+
entryMap map[K]*linked.ListElement[V]
26+
entryList *linked.List[V]
27+
size int
28+
}
29+
30+
// Deprecated: Remove this once the vertex state no longer uses it.
31+
func NewDeduplicator[K comparable, V Evictable[K]](size int) *Deduplicator[K, V] {
32+
return &Deduplicator[K, V]{
33+
entryMap: make(map[K]*linked.ListElement[V]),
34+
entryList: linked.NewList[V](),
35+
size: max(size, 1),
36+
}
37+
}
38+
39+
// Deduplicate returns either the provided value, or a previously provided value
40+
// with the same ID that hasn't yet been evicted
41+
func (d *Deduplicator[_, V]) Deduplicate(value V) V {
42+
d.lock.Lock()
43+
defer d.lock.Unlock()
44+
45+
key := value.Key()
46+
if e, ok := d.entryMap[key]; !ok {
47+
if d.entryList.Len() >= d.size {
48+
e = d.entryList.Front()
49+
d.entryList.MoveToBack(e)
50+
51+
delete(d.entryMap, e.Value.Key())
52+
e.Value.Evict()
53+
54+
e.Value = value
55+
} else {
56+
e = &linked.ListElement[V]{
57+
Value: value,
58+
}
59+
d.entryList.PushBack(e)
60+
}
61+
d.entryMap[key] = e
62+
} else {
63+
d.entryList.MoveToBack(e)
64+
65+
value = e.Value
66+
}
67+
return value
68+
}
69+
70+
// Flush removes all entries from the cache
71+
func (d *Deduplicator[_, _]) Flush() {
72+
d.lock.Lock()
73+
defer d.lock.Unlock()
74+
75+
for d.entryList.Len() > 0 {
76+
e := d.entryList.Front()
77+
d.entryList.Remove(e)
78+
79+
delete(d.entryMap, e.Value.Key())
80+
e.Value.Evict()
81+
}
82+
}
Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
22
// See the file LICENSE for licensing terms.
33

4-
package cache
4+
package lru
55

66
import (
77
"testing"
@@ -24,10 +24,10 @@ func (e *evictable[_]) Evict() {
2424
e.evicted++
2525
}
2626

27-
func TestEvictableLRU(t *testing.T) {
27+
func TestDeduplicator(t *testing.T) {
2828
require := require.New(t)
2929

30-
cache := EvictableLRU[ids.ID, *evictable[ids.ID]]{}
30+
cache := NewDeduplicator[ids.ID, *evictable[ids.ID]](1)
3131

3232
expectedValue1 := &evictable[ids.ID]{id: ids.ID{1}}
3333
require.Equal(expectedValue1, cache.Deduplicate(expectedValue1))
@@ -40,17 +40,4 @@ func TestEvictableLRU(t *testing.T) {
4040
require.Equal(expectedValue2, returnedValue)
4141
require.Equal(1, expectedValue1.evicted)
4242
require.Zero(expectedValue2.evicted)
43-
44-
cache.Size = 2
45-
46-
expectedValue3 := &evictable[ids.ID]{id: ids.ID{2}}
47-
returnedValue = cache.Deduplicate(expectedValue3)
48-
require.Equal(expectedValue2, returnedValue)
49-
require.Equal(1, expectedValue1.evicted)
50-
require.Zero(expectedValue2.evicted)
51-
52-
cache.Flush()
53-
require.Equal(1, expectedValue1.evicted)
54-
require.Equal(1, expectedValue2.evicted)
55-
require.Zero(expectedValue3.evicted)
5643
}

0 commit comments

Comments
 (0)