Skip to content

Commit 2efe971

Browse files
authored
Merge pull request #23 from Code-Hex/fix/janitor-logic
fixed cache janitor logic
2 parents 18d286f + 5463ae1 commit 2efe971

File tree

6 files changed

+180
-87
lines changed

6 files changed

+180
-87
lines changed

cache.go

Lines changed: 52 additions & 69 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,14 @@ 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+
cache Interface[K, *Item[K, V]]
95+
janitorInterval time.Duration
10496
}
10597

10698
func newOptions[K comparable, V any]() *options[K, V] {
10799
return &options[K, V]{
108-
cache: simple.NewCache[K, *Item[K, V]](),
100+
cache: simple.NewCache[K, *Item[K, V]](),
101+
janitorInterval: time.Minute,
109102
}
110103
}
111104

@@ -144,56 +137,48 @@ func AsClock[K comparable, V any](opts ...clock.Option) Option[K, V] {
144137
}
145138
}
146139

140+
// WithJanitorInterval is an option to specify how often cache should delete expired items.
141+
//
142+
// Default is 1 minute.
143+
func WithJanitorInterval[K comparable, V any](d time.Duration) Option[K, V] {
144+
return func(o *options[K, V]) {
145+
o.janitorInterval = d
146+
}
147+
}
148+
147149
// New creates a new thread safe Cache.
150+
// This function will be stopped an internal janitor when the cache is
151+
// no longer referenced anywhere.
148152
//
149153
// There are several Cache replacement policies available with you specified any options.
150154
func New[K comparable, V any](opts ...Option[K, V]) *Cache[K, V] {
151155
o := newOptions[K, V]()
152156
for _, optFunc := range opts {
153157
optFunc(o)
154158
}
155-
159+
ctx, cancel := context.WithCancel(context.Background())
156160
cache := &Cache[K, V]{
157-
cache: o.cache,
161+
cache: o.cache,
162+
janitor: newJanitor(ctx, o.janitorInterval),
158163
}
159-
160-
// @TODO change the ticker timer default value
161-
cache.runJanitor(cache, time.Minute)
162-
runtime.SetFinalizer(cache, cache.stopJanitor)
163-
164+
runtime.SetFinalizer(cache, func(self *Cache[K, V]) {
165+
cancel()
166+
})
164167
return cache
165168
}
166169

167-
func (_ *Cache[K, V]) stopJanitor(c *Cache[K, V]) {
168-
if c.janitor != nil {
169-
c.janitor.stop <- true
170+
// NewContext creates a new thread safe Cache with context.
171+
//
172+
// There are several Cache replacement policies available with you specified any options.
173+
func NewContext[K comparable, V any](ctx context.Context, opts ...Option[K, V]) *Cache[K, V] {
174+
o := newOptions[K, V]()
175+
for _, optFunc := range opts {
176+
optFunc(o)
170177
}
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),
178+
return &Cache[K, V]{
179+
cache: o.cache,
180+
janitor: newJanitor(ctx, o.janitorInterval),
181181
}
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-
}()
197182
}
198183

199184
// Get looks up a key's value from the cache.
@@ -206,9 +191,9 @@ func (c *Cache[K, V]) Get(key K) (value V, ok bool) {
206191
return
207192
}
208193

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

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

218203
// DeleteExpired all expired items from the cache.
219204
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)
205+
for _, key := range c.cache.Keys() {
206+
// if is expired, delete it and return nil instead
207+
item, ok := c.cache.Get(key)
208+
if ok && item.Expired() {
209+
c.cache.Delete(key)
210+
}
223211
}
224212
}
225213

@@ -228,11 +216,6 @@ func (c *Cache[K, V]) Set(key K, val V, opts ...ItemOption) {
228216
c.mu.Lock()
229217
defer c.mu.Unlock()
230218
item := newItem(key, val, opts...)
231-
if item.Expiration <= 0 {
232-
c.cache.Set(key, item)
233-
return
234-
}
235-
236219
c.cache.Set(key, item)
237220
}
238221

cache_internal_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package cache
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
)
8+
9+
func TestDeletedCache(t *testing.T) {
10+
ctx, cancel := context.WithCancel(context.Background())
11+
defer cancel()
12+
13+
nc := NewContext[string, int](ctx)
14+
key := "key"
15+
nc.Set(key, 1, WithExpiration(-time.Second))
16+
17+
_, ok := nc.cache.Get(key)
18+
if !ok {
19+
t.Fatal("want true")
20+
}
21+
22+
nc.DeleteExpired()
23+
24+
_, ok = nc.cache.Get(key)
25+
if ok {
26+
t.Fatal("want false")
27+
}
28+
}

cache_test.go

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"math/rand"
55
"sync"
66
"testing"
7-
"time"
87

98
cache "github.com/Code-Hex/go-generics-cache"
109
"github.com/Code-Hex/go-generics-cache/policy/clock"
@@ -14,23 +13,6 @@ import (
1413
"github.com/Code-Hex/go-generics-cache/policy/mru"
1514
)
1615

17-
func TestExpiration(t *testing.T) {
18-
nc := cache.New[string, int]()
19-
nc.Set("hello", 1, cache.WithExpiration(3*time.Second))
20-
21-
time.Sleep(time.Second * 1)
22-
result, got := nc.Get("hello")
23-
if !got || result != 1 {
24-
t.Errorf("no, expiration must exists")
25-
}
26-
27-
time.Sleep(time.Second * 3)
28-
result, got = nc.Get("hello")
29-
if got || result == 1 {
30-
t.Errorf("no, expiration must not exists")
31-
}
32-
}
33-
3416
func TestMultiThreadIncr(t *testing.T) {
3517
nc := cache.NewNumber[string, int]()
3618
nc.Set("counter", 0)

example_test.go

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

33
import (
4+
"context"
45
"fmt"
56
"time"
67

@@ -20,6 +21,23 @@ func ExampleCache() {
2021
// 0 false
2122
}
2223

24+
func ExampleNewContext() {
25+
ctx, cancel := context.WithCancel(context.Background())
26+
defer cancel()
27+
28+
// use simple cache algorithm without options.
29+
// an internal janitor will be stopped if specified the context is cancelled.
30+
c := cache.NewContext(ctx, cache.WithJanitorInterval[string, int](3*time.Second))
31+
c.Set("a", 1)
32+
gota, aok := c.Get("a")
33+
gotb, bok := c.Get("b")
34+
fmt.Println(gota, aok)
35+
fmt.Println(gotb, bok)
36+
// Output:
37+
// 1 true
38+
// 0 false
39+
}
40+
2341
func ExampleAsClock() {
2442
// use clock cache algorithm.
2543
c := cache.New(cache.AsClock[string, int]())

janitor.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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+
cleanup() // last call
40+
return
41+
case <-j.ctx.Done():
42+
j.stop()
43+
}
44+
}
45+
}()
46+
}

janitor_test.go

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

0 commit comments

Comments
 (0)