Skip to content

Commit ea28bca

Browse files
committed
call onEvict after unlock to avoid deadlocks
1 parent 8c9057a commit ea28bca

File tree

2 files changed

+73
-16
lines changed

2 files changed

+73
-16
lines changed

cache/lru/cache.go

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,27 @@ func NewCache[K comparable, V any](size int) *Cache[K, V] {
4141

4242
func (c *Cache[K, V]) Put(key K, value V) {
4343
c.lock.Lock()
44-
defer c.lock.Unlock()
44+
45+
var evictedKey K
46+
var evictedValue V
47+
evicted := false
4548

4649
if c.elements.Len() == c.size {
47-
oldestKey, oldestValue, _ := c.elements.Oldest()
50+
oldestKey, oldestValue, ok := c.elements.Oldest()
4851
c.elements.Delete(oldestKey)
49-
if c.onEvict != nil {
50-
c.onEvict(oldestKey, oldestValue)
52+
if ok {
53+
evicted = true
54+
evictedKey = oldestKey
55+
evictedValue = oldestValue
5156
}
5257
}
5358
c.elements.Put(key, value)
59+
c.lock.Unlock()
60+
61+
// unlock before calling onEvict to avoid deadlocks
62+
if evicted && c.onEvict != nil {
63+
c.onEvict(evictedKey, evictedValue)
64+
}
5465
}
5566

5667
func (c *Cache[K, V]) Get(key K) (V, bool) {
@@ -65,29 +76,38 @@ func (c *Cache[K, V]) Get(key K) (V, bool) {
6576
return val, true
6677
}
6778

68-
func (c *Cache[K, _]) Evict(key K) {
79+
func (c *Cache[K, V]) Evict(key K) {
6980
c.lock.Lock()
70-
defer c.lock.Unlock()
71-
c.elements.Delete(key)
81+
82+
var evictedValue V
83+
var found bool
7284
if c.onEvict != nil {
73-
value, _ := c.elements.Get(key)
74-
c.onEvict(key, value)
85+
evictedValue, found = c.elements.Get(key)
86+
}
87+
c.elements.Delete(key)
88+
c.lock.Unlock()
89+
90+
if found {
91+
c.onEvict(key, evictedValue)
7592
}
7693
}
7794

78-
func (c *Cache[_, _]) Flush() {
95+
func (c *Cache[K, V]) Flush() {
7996
c.lock.Lock()
80-
defer c.lock.Unlock()
81-
82-
// Call onEvict for each element before clearing
97+
var evicted map[K]V
8398
if c.onEvict != nil {
99+
evicted = make(map[K]V, c.elements.Len())
84100
iter := c.elements.NewIterator()
85101
for iter.Next() {
86-
c.onEvict(iter.Key(), iter.Value())
102+
evicted[iter.Key()] = iter.Value()
87103
}
88104
}
89-
90105
c.elements.Clear()
106+
c.lock.Unlock()
107+
108+
for key, value := range evicted {
109+
c.onEvict(key, value)
110+
}
91111
}
92112

93113
func (c *Cache[_, _]) Len() int {

cache/lru/cache_test.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/stretchr/testify/require"
1010

11+
"github.com/ava-labs/avalanchego/cache"
1112
"github.com/ava-labs/avalanchego/cache/cachetest"
1213
"github.com/ava-labs/avalanchego/ids"
1314
)
@@ -25,7 +26,6 @@ func TestCacheEviction(t *testing.T) {
2526
func TestCacheFlushWithOnEvict(t *testing.T) {
2627
c := NewCache[ids.ID, int64](2)
2728

28-
// Track which elements were evicted
2929
evicted := make(map[ids.ID]int64)
3030
c.SetOnEvict(func(key ids.ID, value int64) {
3131
evicted[key] = value
@@ -48,3 +48,40 @@ func TestCachePutWithOnEvict(t *testing.T) {
4848
require.Len(t, evicted, 1)
4949
require.Equal(t, int64(1), evicted[ids.ID{1}])
5050
}
51+
52+
func TestCacheDeadlockPrevention(t *testing.T) {
53+
tests := []struct {
54+
name string
55+
size int
56+
testFunc func(t *testing.T, c cache.Cacher[ids.ID, int64])
57+
}{
58+
{
59+
name: "Basic",
60+
size: 1,
61+
testFunc: cachetest.Basic,
62+
},
63+
{
64+
name: "Eviction",
65+
size: 2,
66+
testFunc: cachetest.Eviction,
67+
},
68+
}
69+
70+
for _, tt := range tests {
71+
t.Run(tt.name, func(t *testing.T) {
72+
c := NewCache[ids.ID, int64](tt.size)
73+
callbackExecuted := false
74+
75+
c.SetOnEvict(func(key ids.ID, value int64) {
76+
// This would deadlock if the lock wasn't released before calling the callback
77+
c.Evict(ids.ID{99}) // Try to delete something from the callback
78+
callbackExecuted = true
79+
})
80+
81+
tt.testFunc(t, c)
82+
83+
// If we get here without deadlock, the test passes
84+
require.True(t, callbackExecuted, "Callback should have been executed")
85+
})
86+
}
87+
}

0 commit comments

Comments
 (0)