Skip to content

Commit 3452e4e

Browse files
committed
Fix memory leak
As documented in #76, an entry which is both GC'd and deleted (either via a delete or an update) will result in the internal link list having a nil tail (because removing the same node multiple times from the linked list does that). doDelete was already aware of "invalid" nodes (where item.node == nil), so the solution seems to be as simple as setting item.node = nil during GC.
1 parent 4d3a63d commit 3452e4e

File tree

4 files changed

+50
-0
lines changed

4 files changed

+50
-0
lines changed

cache.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,7 @@ func (c *Cache[T]) gc() int {
438438
c.onDelete(item)
439439
}
440440
dropped += 1
441+
item.node = nil
441442
item.promotions = -2
442443
}
443444
node = prev

cache_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package ccache
22

33
import (
4+
"math/rand"
45
"sort"
56
"strconv"
67
"sync/atomic"
@@ -313,6 +314,29 @@ func Test_CacheForEachFunc(t *testing.T) {
313314
assert.DoesNotContain(t, forEachKeys(cache), "stop")
314315
}
315316

317+
func Test_CachePrune(t *testing.T) {
318+
maxSize := int64(500)
319+
cache := New(Configure[string]().MaxSize(maxSize).ItemsToPrune(50))
320+
epoch := 0
321+
for i := 0; i < 10000; i++ {
322+
epoch += 1
323+
expired := make([]string, 0)
324+
for i := 0; i < 50; i += 1 {
325+
key := strconv.FormatInt(rand.Int63n(maxSize*20), 10)
326+
item := cache.Get(key)
327+
if item == nil || item.TTL() > 1*time.Minute {
328+
expired = append(expired, key)
329+
}
330+
}
331+
for _, key := range expired {
332+
cache.Set(key, key, 5*time.Minute)
333+
}
334+
if epoch%500 == 0 {
335+
assert.True(t, cache.GetSize() < 500)
336+
}
337+
}
338+
}
339+
316340
type SizedItem struct {
317341
id int
318342
s int64

layeredcache.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,7 @@ func (c *LayeredCache[T]) gc() int {
355355
if c.onDelete != nil {
356356
c.onDelete(item)
357357
}
358+
item.node = nil
358359
item.promotions = -2
359360
dropped += 1
360361
}

layeredcache_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package ccache
22

33
import (
4+
"math/rand"
45
"sort"
56
"strconv"
67
"sync/atomic"
@@ -372,6 +373,29 @@ func Test_LayeredCache_EachFunc(t *testing.T) {
372373
assert.DoesNotContain(t, forEachKeysLayered[int](cache, "1"), "stop")
373374
}
374375

376+
func Test_LayeredCachePrune(t *testing.T) {
377+
maxSize := int64(500)
378+
cache := Layered(Configure[string]().MaxSize(maxSize).ItemsToPrune(50))
379+
epoch := 0
380+
for i := 0; i < 10000; i++ {
381+
epoch += 1
382+
expired := make([]string, 0)
383+
for i := 0; i < 50; i += 1 {
384+
key := strconv.FormatInt(rand.Int63n(maxSize*20), 10)
385+
item := cache.Get(key, key)
386+
if item == nil || item.TTL() > 1*time.Minute {
387+
expired = append(expired, key)
388+
}
389+
}
390+
for _, key := range expired {
391+
cache.Set(key, key, key, 5*time.Minute)
392+
}
393+
if epoch%500 == 0 {
394+
assert.True(t, cache.GetSize() < 500)
395+
}
396+
}
397+
}
398+
375399
func newLayered[T any]() *LayeredCache[T] {
376400
c := Layered[T](Configure[T]())
377401
c.Clear()

0 commit comments

Comments
 (0)