Skip to content

Commit 6d2af3f

Browse files
authored
Merge pull request #99 from pior/memleak
Fix single-item cache bug
2 parents 2c9c102 + e48e1e8 commit 6d2af3f

File tree

6 files changed

+86
-5
lines changed

6 files changed

+86
-5
lines changed

cache.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ doAllDeletes:
331331
}
332332

333333
func (c *Cache[T]) doDelete(item *Item[T]) {
334-
if item.next == nil && item.prev == nil {
334+
if !item.inList {
335335
item.promotions = -2
336336
} else {
337337
c.size -= item.size
@@ -344,12 +344,12 @@ func (c *Cache[T]) doDelete(item *Item[T]) {
344344
}
345345

346346
func (c *Cache[T]) doPromote(item *Item[T]) bool {
347-
//already deleted
347+
// already deleted
348348
if item.promotions == -2 {
349349
return false
350350
}
351351

352-
if item.next != nil || item.prev != nil { // not a new item
352+
if item.inList {
353353
if item.shouldPromote(c.getsPerPromote) {
354354
c.list.MoveToFront(item)
355355
item.promotions = 0

cache_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,49 @@ func Test_CacheOnDeleteCallbackCalled(t *testing.T) {
150150
assert.Equal(t, atomic.LoadInt32(&onDeleteFnCalled), 1)
151151
}
152152

153+
func Test_CacheSingleItemSizeAccounting(t *testing.T) {
154+
cache := New(Configure[*SizedItem]().GetsPerPromote(1))
155+
defer cache.Stop()
156+
157+
// Add a single item to the cache
158+
cache.Set("only", &SizedItem{1, 10}, time.Minute)
159+
cache.SyncUpdates()
160+
assert.Equal(t, cache.GetSize(), 10)
161+
162+
// Get the item to trigger promotion
163+
// With a single item, next==nil && prev==nil, so it may be
164+
// incorrectly treated as a "new" item and size double-counted
165+
cache.Get("only")
166+
cache.Get("only")
167+
cache.SyncUpdates()
168+
assert.Equal(t, cache.GetSize(), 10) // Should still be 10, not 20 or 30
169+
}
170+
171+
func Test_CacheSingleItemDelete(t *testing.T) {
172+
onDeleteCalled := int32(0)
173+
onDeleteFn := func(item *Item[string]) {
174+
atomic.AddInt32(&onDeleteCalled, 1)
175+
}
176+
177+
cache := New(Configure[string]().OnDelete(onDeleteFn))
178+
defer cache.Stop()
179+
180+
// Add a single item
181+
cache.Set("only", "value", time.Minute)
182+
cache.SyncUpdates()
183+
assert.Equal(t, cache.GetSize(), 1)
184+
185+
// Delete the only item in the cache
186+
// With a single item, next==nil && prev==nil, so the delete
187+
// path may incorrectly skip size decrement and onDelete callback
188+
cache.Delete("only")
189+
cache.SyncUpdates()
190+
191+
assert.Equal(t, cache.Get("only"), nil)
192+
assert.Equal(t, atomic.LoadInt32(&onDeleteCalled), 1) // onDelete should be called
193+
assert.Equal(t, cache.GetSize(), 0) // Size should be 0
194+
}
195+
153196
func Test_CacheFetchesExpiredItems(t *testing.T) {
154197
cache := New(Configure[string]())
155198
defer cache.Stop()

item.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type Item[T any] struct {
2929
value T
3030
next *Item[T]
3131
prev *Item[T]
32+
inList bool
3233
}
3334

3435
func newItem[T any](key string, value T, expires int64, track bool) *Item[T] {

layeredcache.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ drain:
293293
}
294294

295295
func (c *LayeredCache[T]) doDelete(item *Item[T]) {
296-
if item.prev == nil && item.next == nil {
296+
if !item.inList {
297297
item.promotions = -2
298298
} else {
299299
c.size -= item.size
@@ -311,7 +311,7 @@ func (c *LayeredCache[T]) doPromote(item *Item[T]) bool {
311311
return false
312312
}
313313

314-
if item.next != nil || item.prev != nil { // not a new item
314+
if item.inList {
315315
if item.shouldPromote(c.getsPerPromote) {
316316
c.list.MoveToFront(item)
317317
item.promotions = 0

layeredcache_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,41 @@ func Test_LayedCache_OnDeleteCallbackCalled(t *testing.T) {
163163
assert.Equal(t, atomic.LoadInt32(&onDeleteFnCalled), 1)
164164
}
165165

166+
func Test_LayeredCache_SingleItemSizeAccounting(t *testing.T) {
167+
cache := Layered(Configure[*SizedItem]().GetsPerPromote(1))
168+
defer cache.Stop()
169+
170+
cache.Set("group", "only", &SizedItem{1, 10}, time.Minute)
171+
cache.SyncUpdates()
172+
assert.Equal(t, cache.GetSize(), 10)
173+
174+
cache.Get("group", "only")
175+
cache.Get("group", "only")
176+
cache.SyncUpdates()
177+
assert.Equal(t, cache.GetSize(), 10)
178+
}
179+
180+
func Test_LayeredCache_SingleItemDelete(t *testing.T) {
181+
onDeleteCalled := int32(0)
182+
onDeleteFn := func(item *Item[string]) {
183+
atomic.AddInt32(&onDeleteCalled, 1)
184+
}
185+
186+
cache := Layered(Configure[string]().OnDelete(onDeleteFn))
187+
defer cache.Stop()
188+
189+
cache.Set("group", "only", "value", time.Minute)
190+
cache.SyncUpdates()
191+
assert.Equal(t, cache.GetSize(), 1)
192+
193+
cache.Delete("group", "only")
194+
cache.SyncUpdates()
195+
196+
assert.Equal(t, cache.Get("group", "only"), nil)
197+
assert.Equal(t, atomic.LoadInt32(&onDeleteCalled), 1)
198+
assert.Equal(t, cache.GetSize(), 0)
199+
}
200+
166201
func Test_LayedCache_DeletesALayer(t *testing.T) {
167202
cache := newLayered[string]()
168203
defer cache.Stop()

list.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ func (l *List[T]) Remove(item *Item[T]) {
2626
}
2727
item.next = nil
2828
item.prev = nil
29+
item.inList = false
2930
}
3031

3132
func (l *List[T]) MoveToFront(item *Item[T]) {
@@ -36,6 +37,7 @@ func (l *List[T]) MoveToFront(item *Item[T]) {
3637
func (l *List[T]) Insert(item *Item[T]) {
3738
head := l.Head
3839
l.Head = item
40+
item.inList = true
3941
if head == nil {
4042
l.Tail = item
4143
return

0 commit comments

Comments
 (0)