Skip to content

Commit 04fe7d7

Browse files
committed
fixed cache janitor logic
1 parent 18d286f commit 04fe7d7

File tree

3 files changed

+134
-68
lines changed

3 files changed

+134
-68
lines changed

cache.go

Lines changed: 55 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cache
22

33
import (
4+
"context"
45
"runtime"
56
"sync"
67
"time"
@@ -13,15 +14,6 @@ import (
1314
"github.com/Code-Hex/go-generics-cache/policy/simple"
1415
)
1516

16-
// janitor for collecting expired items and cleaning them
17-
// this object is inspired from
18-
// https://github.com/patrickmn/go-cache/blob/46f407853014144407b6c2ec7ccc76bf67958d93/cache.go
19-
// many thanks to go-cache project
20-
type janitor struct {
21-
Interval time.Duration
22-
stop chan bool
23-
}
24-
2517
// Interface is a common-cache interface.
2618
type Interface[K comparable, V any] interface {
2719
Get(key K) (value V, ok bool)
@@ -45,7 +37,15 @@ var (
4537
type Item[K comparable, V any] struct {
4638
Key K
4739
Value V
48-
Expiration int64
40+
Expiration time.Time
41+
}
42+
43+
// Expired returns true if the item has expired.
44+
func (item *Item[K, V]) Expired() bool {
45+
if item.Expiration.IsZero() {
46+
return false
47+
}
48+
return nowFunc().After(item.Expiration)
4949
}
5050

5151
var nowFunc = time.Now
@@ -54,23 +54,14 @@ var nowFunc = time.Now
5454
type ItemOption func(*itemOptions)
5555

5656
type itemOptions struct {
57-
expiration int64 // default none
58-
}
59-
60-
// Expired returns true if the item has expired.
61-
func (item itemOptions) Expired() bool {
62-
if item.expiration == 0 {
63-
return false
64-
}
65-
66-
return nowFunc().UnixNano() > item.expiration
57+
expiration time.Time // default none
6758
}
6859

6960
// WithExpiration is an option to set expiration time for any items.
7061
// If the expiration is zero or negative value, it treats as w/o expiration.
7162
func WithExpiration(exp time.Duration) ItemOption {
7263
return func(o *itemOptions) {
73-
o.expiration = nowFunc().Add(exp).UnixNano()
64+
o.expiration = nowFunc().Add(exp)
7465
}
7566
}
7667

@@ -100,12 +91,16 @@ type Cache[K comparable, V any] struct {
10091
type Option[K comparable, V any] func(*options[K, V])
10192

10293
type options[K comparable, V any] struct {
103-
cache Interface[K, *Item[K, V]]
94+
ctx context.Context
95+
cache Interface[K, *Item[K, V]]
96+
janitorInterval time.Duration
10497
}
10598

10699
func newOptions[K comparable, V any]() *options[K, V] {
107100
return &options[K, V]{
108-
cache: simple.NewCache[K, *Item[K, V]](),
101+
ctx: context.Background(),
102+
cache: simple.NewCache[K, *Item[K, V]](),
103+
janitorInterval: time.Minute,
109104
}
110105
}
111106

@@ -144,6 +139,15 @@ func AsClock[K comparable, V any](opts ...clock.Option) Option[K, V] {
144139
}
145140
}
146141

142+
// WithJanitorInterval is an option to specify how often cache should delete expired items.
143+
//
144+
// Default is 1 minute.
145+
func WithJanitorInterval[K comparable, V any](d time.Duration) Option[K, V] {
146+
return func(o *options[K, V]) {
147+
o.janitorInterval = d
148+
}
149+
}
150+
147151
// New creates a new thread safe Cache.
148152
//
149153
// There are several Cache replacement policies available with you specified any options.
@@ -152,48 +156,33 @@ func New[K comparable, V any](opts ...Option[K, V]) *Cache[K, V] {
152156
for _, optFunc := range opts {
153157
optFunc(o)
154158
}
155-
156159
cache := &Cache[K, V]{
157160
cache: o.cache,
158161
}
159-
160-
// @TODO change the ticker timer default value
161-
cache.runJanitor(cache, time.Minute)
162-
runtime.SetFinalizer(cache, cache.stopJanitor)
163-
162+
if o.ctx == context.Background() {
163+
ctx, cancel := context.WithCancel(o.ctx)
164+
cache.janitor = newJanitor(ctx, o.janitorInterval)
165+
runtime.SetFinalizer(cache, func(self *Cache[K, V]) {
166+
cancel()
167+
})
168+
} else {
169+
cache.janitor = newJanitor(o.ctx, o.janitorInterval)
170+
}
164171
return cache
165172
}
166173

167-
func (_ *Cache[K, V]) stopJanitor(c *Cache[K, V]) {
168-
if c.janitor != nil {
169-
c.janitor.stop <- true
174+
// NewContext creates a new thread safe Cache with context.
175+
//
176+
// There are several Cache replacement policies available with you specified any options.
177+
func NewContext[K comparable, V any](ctx context.Context, opts ...Option[K, V]) *Cache[K, V] {
178+
o := newOptions[K, V]()
179+
for _, optFunc := range opts {
180+
optFunc(o)
170181
}
171-
172-
c.janitor = nil
173-
}
174-
175-
func (_ *Cache[K, V]) runJanitor(c *Cache[K, V], ci time.Duration) {
176-
c.stopJanitor(c)
177-
178-
j := &janitor{
179-
Interval: ci,
180-
stop: make(chan bool),
182+
return &Cache[K, V]{
183+
cache: o.cache,
184+
janitor: newJanitor(o.ctx, o.janitorInterval),
181185
}
182-
183-
c.janitor = j
184-
185-
go func() {
186-
ticker := time.NewTicker(j.Interval)
187-
for {
188-
select {
189-
case <-ticker.C:
190-
c.DeleteExpired()
191-
case <-j.stop:
192-
ticker.Stop()
193-
return
194-
}
195-
}
196-
}()
197186
}
198187

199188
// Get looks up a key's value from the cache.
@@ -206,9 +195,9 @@ func (c *Cache[K, V]) Get(key K) (value V, ok bool) {
206195
return
207196
}
208197

209-
// if is expired, delete is and return nil instead
210-
if item.Expiration > 0 && nowFunc().UnixNano() > item.Expiration {
211-
c.cache.Delete(key)
198+
// Returns nil if the item has been expired.
199+
// Do not delete here and leave it to an external process such as Janitor.
200+
if item.Expired() {
212201
return value, false
213202
}
214203

@@ -217,9 +206,12 @@ func (c *Cache[K, V]) Get(key K) (value V, ok bool) {
217206

218207
// DeleteExpired all expired items from the cache.
219208
func (c *Cache[K, V]) DeleteExpired() {
220-
for _, keys := range c.cache.Keys() {
221-
// delete all expired items by using get method
222-
_, _ = c.Get(keys)
209+
for _, key := range c.cache.Keys() {
210+
// if is expired, delete it and return nil instead
211+
item, ok := c.cache.Get(key)
212+
if ok && item.Expired() {
213+
c.cache.Delete(key)
214+
}
223215
}
224216
}
225217

@@ -228,11 +220,6 @@ func (c *Cache[K, V]) Set(key K, val V, opts ...ItemOption) {
228220
c.mu.Lock()
229221
defer c.mu.Unlock()
230222
item := newItem(key, val, opts...)
231-
if item.Expiration <= 0 {
232-
c.cache.Set(key, item)
233-
return
234-
}
235-
236223
c.cache.Set(key, item)
237224
}
238225

janitor.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package cache
2+
3+
import (
4+
"context"
5+
"sync"
6+
"time"
7+
)
8+
9+
// janitor for collecting expired items and cleaning them.
10+
type janitor struct {
11+
ctx context.Context
12+
interval time.Duration
13+
done chan struct{}
14+
once sync.Once
15+
}
16+
17+
func newJanitor(ctx context.Context, interval time.Duration) *janitor {
18+
j := &janitor{
19+
ctx: ctx,
20+
interval: interval,
21+
done: make(chan struct{}),
22+
}
23+
return j
24+
}
25+
26+
func (j *janitor) stop() {
27+
j.once.Do(func() { close(j.done) })
28+
}
29+
30+
func (j *janitor) run(cleanup func()) {
31+
go func() {
32+
ticker := time.NewTicker(j.interval)
33+
defer ticker.Stop()
34+
for {
35+
select {
36+
case <-ticker.C:
37+
cleanup()
38+
case <-j.done:
39+
return
40+
case <-j.ctx.Done():
41+
j.stop()
42+
}
43+
}
44+
}()
45+
}

janitor_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package cache
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
)
8+
9+
func TestJanitor(t *testing.T) {
10+
ctx, cancel := context.WithCancel(context.Background())
11+
defer cancel()
12+
13+
janitor := newJanitor(ctx, time.Millisecond)
14+
15+
checkDone := make(chan struct{})
16+
janitor.done = checkDone
17+
18+
calledClean := false
19+
janitor.run(func() { calledClean = true })
20+
21+
// waiting for cleanup
22+
time.Sleep(10 * time.Millisecond)
23+
cancel()
24+
25+
select {
26+
case <-checkDone:
27+
case <-time.After(time.Second):
28+
t.Fatalf("failed to call done channel")
29+
}
30+
31+
if !calledClean {
32+
t.Fatal("failed to call clean callback in janitor")
33+
}
34+
}

0 commit comments

Comments
 (0)