Skip to content

Commit 47fdf0d

Browse files
committed
fix: expiring cache entries during read and write
1 parent aca3e78 commit 47fdf0d

File tree

2 files changed

+102
-5
lines changed

2 files changed

+102
-5
lines changed

cache_test.go

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

33
import (
4+
"fmt"
45
"math/rand"
56
"testing"
67
"time"
@@ -282,6 +283,48 @@ func TestOnExpired(t *testing.T) {
282283
}
283284
}
284285

286+
func TestExpiring(t *testing.T) {
287+
for _, tt := range cacheTests {
288+
t.Run("Test"+tt.cont.String()+"CacheExpiring", func(t *testing.T) {
289+
cache := tt.cont.New(0)
290+
keys := make([]interface{}, 10)
291+
for i := 0; i < 10; i++ {
292+
cache.StoreWithTTL(fmt.Sprintf("%v.100", i), i, time.Millisecond*100)
293+
cache.StoreWithTTL(fmt.Sprintf("%v.200", i), i, time.Millisecond*200)
294+
keys[i] = fmt.Sprintf("%v.200", i)
295+
}
296+
297+
time.Sleep(time.Millisecond * 100)
298+
299+
cache.Peek("notfound") // should expire *.100
300+
got := cache.Keys()
301+
assert.ElementsMatch(t, keys, got)
302+
303+
time.Sleep(time.Millisecond * 100)
304+
cache.Store("notfound", 0) // should expire *.200
305+
got = cache.Keys()
306+
assert.ElementsMatch(t, []string{"notfound"}, got)
307+
308+
cache.Purge()
309+
310+
// check remove element will keep other entries in heap.
311+
// this has been added to make sure we remove right entry
312+
// by its index.
313+
cache.StoreWithTTL(1, 1, time.Millisecond*100)
314+
cache.StoreWithTTL(2, 2, time.Millisecond*200)
315+
316+
cache.Delete(2)
317+
got = cache.Keys()
318+
assert.ElementsMatch(t, []int{1}, got)
319+
320+
time.Sleep(time.Millisecond * 100)
321+
cache.Peek("")
322+
assert.Equal(t, 0, cache.Len())
323+
324+
})
325+
}
326+
}
327+
285328
func BenchmarkCache(b *testing.B) {
286329
for _, tt := range cacheTests {
287330
b.Run("Benchmark"+tt.cont.String()+"Cache", func(b *testing.B) {

internal/cache.go

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package internal
22

33
import (
4+
"container/heap"
45
"time"
56
)
67

@@ -22,6 +23,7 @@ type Entry struct {
2223
Element interface{}
2324
Exp time.Time
2425
timer *time.Timer
26+
index int
2527
cancel chan struct{}
2628
}
2729

@@ -50,6 +52,7 @@ func (e *Entry) stopTimer() {
5052
// of the Cache interface to minimize the effort required to implement interface.
5153
type Cache struct {
5254
coll Collection
55+
heap expiringHeap
5356
entries map[interface{}]*Entry
5457
onEvicted func(key, value interface{})
5558
onExpired func(key, value interface{})
@@ -68,16 +71,14 @@ func (c *Cache) Peek(key interface{}) (interface{}, bool) {
6871
}
6972

7073
func (c *Cache) get(key interface{}, peek bool) (v interface{}, found bool) {
74+
// Run GC inline before return the entry.
75+
c.gc()
76+
7177
e, ok := c.entries[key]
7278
if !ok {
7379
return
7480
}
7581

76-
if !e.Exp.IsZero() && time.Now().UTC().After(e.Exp) {
77-
c.evict(e)
78-
return
79-
}
80-
8182
if !peek {
8283
c.coll.Move(e)
8384
}
@@ -101,6 +102,9 @@ func (c *Cache) Store(key, value interface{}) {
101102

102103
// StoreWithTTL sets the key value with TTL overrides the default.
103104
func (c *Cache) StoreWithTTL(key, value interface{}, ttl time.Duration) {
105+
// Run GC inline before pushing the new entry.
106+
c.gc()
107+
104108
if e, ok := c.entries[key]; ok {
105109
c.removeEntry(e)
106110
}
@@ -112,6 +116,7 @@ func (c *Cache) StoreWithTTL(key, value interface{}, ttl time.Duration) {
112116
e.startTimer(ttl, c.onExpired)
113117
}
114118
e.Exp = time.Now().UTC().Add(ttl)
119+
heap.Push(&c.heap, e)
115120
}
116121

117122
c.entries[key] = e
@@ -205,6 +210,11 @@ func (c *Cache) removeEntry(e *Entry) {
205210
c.coll.Remove(e)
206211
e.stopTimer()
207212
delete(c.entries, e.Key)
213+
// Remove entry from the heap, the entry may does not exist because
214+
// it has zero ttl or already popped up by gc
215+
if len(c.heap) > 0 && e.index < len(c.heap) && e.Key == c.heap[e.index].Key {
216+
heap.Remove(&c.heap, e.index)
217+
}
208218
}
209219

210220
// evict remove entry and fire on evicted callback.
@@ -215,6 +225,19 @@ func (c *Cache) evict(e *Entry) {
215225
}
216226
}
217227

228+
func (c *Cache) gc() {
229+
now := time.Now()
230+
for {
231+
// Return from gc if the heap is empty or the next element is not yet
232+
// expired
233+
if len(c.heap) == 0 || now.Before(c.heap[0].Exp) {
234+
return
235+
}
236+
e := heap.Pop(&c.heap).(*Entry)
237+
c.removeEntry(e)
238+
}
239+
}
240+
218241
// TTL returns entries default TTL.
219242
func (c *Cache) TTL() time.Duration {
220243
return c.ttl
@@ -250,3 +273,34 @@ func New(c Collection, cap int) *Cache {
250273
entries: make(map[interface{}]*Entry),
251274
}
252275
}
276+
277+
// expiringHeap is a min-heap ordered by expiration time of its entries. The
278+
// expiring cache uses this as a priority queue to efficiently organize entries
279+
// which will be garbage collected once they expire.
280+
type expiringHeap []*Entry
281+
282+
var _ heap.Interface = &expiringHeap{}
283+
284+
func (cq expiringHeap) Len() int {
285+
return len(cq)
286+
}
287+
288+
func (cq expiringHeap) Less(i, j int) bool {
289+
return cq[i].Exp.Before(cq[j].Exp)
290+
}
291+
292+
func (cq expiringHeap) Swap(i, j int) {
293+
cq[i].index, cq[j].index = cq[j].index, cq[i].index
294+
cq[i], cq[j] = cq[j], cq[i]
295+
}
296+
297+
func (cq *expiringHeap) Push(c interface{}) {
298+
c.(*Entry).index = len(*cq)
299+
*cq = append(*cq, c.(*Entry))
300+
}
301+
302+
func (cq *expiringHeap) Pop() interface{} {
303+
c := (*cq)[cq.Len()-1]
304+
*cq = (*cq)[:cq.Len()-1]
305+
return c
306+
}

0 commit comments

Comments
 (0)