Skip to content

Commit 5303a9a

Browse files
authored
Merge pull request #48 from Code-Hex/improve/expiration
improved expiration items
2 parents 96a4323 + 1c4a538 commit 5303a9a

File tree

3 files changed

+231
-12
lines changed

3 files changed

+231
-12
lines changed

cache.go

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,13 @@ type Item[K comparable, V any] struct {
4646
InitialReferenceCount int
4747
}
4848

49+
func (item *Item[K, V]) hasExpiration() bool {
50+
return !item.Expiration.IsZero()
51+
}
52+
4953
// Expired returns true if the item has expired.
5054
func (item *Item[K, V]) Expired() bool {
51-
if item.Expiration.IsZero() {
55+
if !item.hasExpiration() {
5256
return false
5357
}
5458
return nowFunc().After(item.Expiration)
@@ -107,8 +111,9 @@ func newItem[K comparable, V any](key K, val V, opts ...ItemOption) *Item[K, V]
107111
type Cache[K comparable, V any] struct {
108112
cache Interface[K, *Item[K, V]]
109113
// mu is used to do lock in some method process.
110-
mu sync.Mutex
111-
janitor *janitor
114+
mu sync.Mutex
115+
janitor *janitor
116+
expManager *expirationManager[K]
112117
}
113118

114119
// Option is an option for cache.
@@ -190,15 +195,16 @@ func NewContext[K comparable, V any](ctx context.Context, opts ...Option[K, V])
190195
optFunc(o)
191196
}
192197
cache := &Cache[K, V]{
193-
cache: o.cache,
194-
janitor: newJanitor(ctx, o.janitorInterval),
198+
cache: o.cache,
199+
janitor: newJanitor(ctx, o.janitorInterval),
200+
expManager: newExpirationManager[K](),
195201
}
196202
cache.janitor.run(cache.DeleteExpired)
197203
return cache
198204
}
199205

200206
// Get looks up a key's value from the cache.
201-
func (c *Cache[K, V]) Get(key K) (value V, ok bool) {
207+
func (c *Cache[K, V]) Get(key K) (zero V, ok bool) {
202208
c.mu.Lock()
203209
defer c.mu.Unlock()
204210
item, ok := c.cache.Get(key)
@@ -210,7 +216,7 @@ func (c *Cache[K, V]) Get(key K) (value V, ok bool) {
210216
// Returns nil if the item has been expired.
211217
// Do not delete here and leave it to an external process such as Janitor.
212218
if item.Expired() {
213-
return value, false
219+
return zero, false
214220
}
215221

216222
return item.Value, true
@@ -219,17 +225,30 @@ func (c *Cache[K, V]) Get(key K) (value V, ok bool) {
219225
// DeleteExpired all expired items from the cache.
220226
func (c *Cache[K, V]) DeleteExpired() {
221227
c.mu.Lock()
222-
keys := c.cache.Keys()
228+
l := c.expManager.len()
223229
c.mu.Unlock()
224230

225-
for _, key := range keys {
226-
c.mu.Lock()
231+
evict := func() bool {
232+
key := c.expManager.pop()
227233
// if is expired, delete it and return nil instead
228234
item, ok := c.cache.Get(key)
229-
if ok && item.Expired() {
230-
c.cache.Delete(key)
235+
if ok {
236+
if item.Expired() {
237+
c.cache.Delete(key)
238+
return false
239+
}
240+
c.expManager.update(key, item.Expiration)
231241
}
242+
return true
243+
}
244+
245+
for i := 0; i < l; i++ {
246+
c.mu.Lock()
247+
shouldBreak := evict()
232248
c.mu.Unlock()
249+
if shouldBreak {
250+
break
251+
}
233252
}
234253
}
235254

@@ -238,6 +257,9 @@ func (c *Cache[K, V]) Set(key K, val V, opts ...ItemOption) {
238257
c.mu.Lock()
239258
defer c.mu.Unlock()
240259
item := newItem(key, val, opts...)
260+
if item.hasExpiration() {
261+
c.expManager.update(key, item.Expiration)
262+
}
241263
c.cache.Set(key, item)
242264
}
243265

@@ -253,6 +275,7 @@ func (c *Cache[K, V]) Delete(key K) {
253275
c.mu.Lock()
254276
defer c.mu.Unlock()
255277
c.cache.Delete(key)
278+
c.expManager.remove(key)
256279
}
257280

258281
// Len returns the number of items in the cache.

cache_internal_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,108 @@ func TestDeletedCache(t *testing.T) {
2626
t.Fatal("want false")
2727
}
2828
}
29+
30+
func TestDeleteExpired(t *testing.T) {
31+
now := time.Now()
32+
restore := func() {
33+
nowFunc = time.Now
34+
}
35+
36+
t.Run("normal", func(t *testing.T) {
37+
defer restore()
38+
c := New[string, int]()
39+
40+
c.Set("0", 0)
41+
c.Set("1", 10, WithExpiration(10*time.Millisecond))
42+
c.Set("2", 20, WithExpiration(20*time.Millisecond))
43+
c.Set("3", 30, WithExpiration(30*time.Millisecond))
44+
c.Set("4", 40, WithExpiration(40*time.Millisecond))
45+
c.Set("5", 50)
46+
47+
maxItems := c.Len()
48+
49+
expItems := 2
50+
51+
for i := 0; i <= maxItems; i++ {
52+
nowFunc = func() time.Time {
53+
// Advance time to expire some items
54+
advanced := time.Duration(i * 10)
55+
return now.Add(advanced * time.Millisecond).Add(time.Millisecond)
56+
}
57+
58+
c.DeleteExpired()
59+
60+
got := c.Len()
61+
want := max(maxItems-i, expItems)
62+
if want != got {
63+
t.Errorf("want %d items but got %d", want, got)
64+
}
65+
}
66+
})
67+
68+
t.Run("with remove", func(t *testing.T) {
69+
defer restore()
70+
c := New[string, int]()
71+
72+
c.Set("0", 0)
73+
c.Set("1", 10, WithExpiration(10*time.Millisecond))
74+
c.Set("2", 20, WithExpiration(20*time.Millisecond))
75+
76+
c.Delete("1")
77+
78+
nowFunc = func() time.Time {
79+
return now.Add(30 * time.Millisecond).Add(time.Millisecond)
80+
}
81+
82+
c.DeleteExpired()
83+
84+
keys := c.Keys()
85+
want := 1
86+
if want != len(keys) {
87+
t.Errorf("want %d items but got %d", want, len(keys))
88+
}
89+
})
90+
91+
t.Run("with update", func(t *testing.T) {
92+
defer restore()
93+
c := New[string, int]()
94+
95+
c.Set("0", 0)
96+
c.Set("1", 10, WithExpiration(10*time.Millisecond))
97+
c.Set("2", 20, WithExpiration(20*time.Millisecond))
98+
c.Set("1", 30, WithExpiration(30*time.Millisecond)) // update
99+
100+
maxItems := c.Len()
101+
102+
nowFunc = func() time.Time {
103+
return now.Add(10 * time.Millisecond).Add(time.Millisecond)
104+
}
105+
106+
c.DeleteExpired()
107+
108+
got1 := c.Len()
109+
want1 := maxItems
110+
if want1 != got1 {
111+
t.Errorf("want1 %d items but got1 %d", want1, got1)
112+
}
113+
114+
nowFunc = func() time.Time {
115+
return now.Add(30 * time.Millisecond).Add(time.Millisecond)
116+
}
117+
118+
c.DeleteExpired()
119+
120+
got2 := c.Len()
121+
want2 := 1
122+
if want2 != got2 {
123+
t.Errorf("want2 %d items but got2 %d", want2, got2)
124+
}
125+
})
126+
}
127+
128+
func max(x, y int) int {
129+
if x < y {
130+
return y
131+
}
132+
return x
133+
}

expiration.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package cache
2+
3+
import (
4+
"container/heap"
5+
"time"
6+
)
7+
8+
type expirationManager[K comparable] struct {
9+
queue expirationQueue[K]
10+
mapping map[K]*expirationKey[K]
11+
}
12+
13+
func newExpirationManager[K comparable]() *expirationManager[K] {
14+
q := make(expirationQueue[K], 0)
15+
heap.Init(&q)
16+
return &expirationManager[K]{
17+
queue: q,
18+
mapping: make(map[K]*expirationKey[K]),
19+
}
20+
}
21+
22+
func (m *expirationManager[K]) update(key K, expiration time.Time) {
23+
if e, ok := m.mapping[key]; ok {
24+
heap.Fix(&m.queue, e.index)
25+
} else {
26+
v := &expirationKey[K]{
27+
key: key,
28+
expiration: expiration,
29+
}
30+
heap.Push(&m.queue, v)
31+
m.mapping[key] = v
32+
}
33+
}
34+
35+
func (m *expirationManager[K]) len() int {
36+
return m.queue.Len()
37+
}
38+
39+
func (m *expirationManager[K]) pop() K {
40+
v := heap.Pop(&m.queue)
41+
key := v.(*expirationKey[K]).key
42+
delete(m.mapping, key)
43+
return key
44+
}
45+
46+
func (m *expirationManager[K]) remove(key K) {
47+
if e, ok := m.mapping[key]; ok {
48+
heap.Remove(&m.queue, e.index)
49+
delete(m.mapping, key)
50+
}
51+
}
52+
53+
type expirationKey[K comparable] struct {
54+
key K
55+
expiration time.Time
56+
index int
57+
}
58+
59+
// expirationQueue implements heap.Interface and holds CacheItems.
60+
type expirationQueue[K comparable] []*expirationKey[K]
61+
62+
var _ heap.Interface = (*expirationQueue[int])(nil)
63+
64+
func (pq expirationQueue[K]) Len() int { return len(pq) }
65+
66+
func (pq expirationQueue[K]) Less(i, j int) bool {
67+
// We want Pop to give us the least based on expiration time, not the greater
68+
return pq[i].expiration.Before(pq[j].expiration)
69+
}
70+
71+
func (pq expirationQueue[K]) Swap(i, j int) {
72+
pq[i], pq[j] = pq[j], pq[i]
73+
pq[i].index = i
74+
pq[j].index = j
75+
}
76+
77+
func (pq *expirationQueue[K]) Push(x interface{}) {
78+
n := len(*pq)
79+
item := x.(*expirationKey[K])
80+
item.index = n
81+
*pq = append(*pq, item)
82+
}
83+
84+
func (pq *expirationQueue[K]) Pop() interface{} {
85+
old := *pq
86+
n := len(old)
87+
item := old[n-1]
88+
item.index = -1 // For safety
89+
*pq = old[0 : n-1]
90+
return item
91+
}

0 commit comments

Comments
 (0)