Skip to content

Commit 18d286f

Browse files
authored
Merge pull request #22 from mingcheng/main
2 parents a0612be + ccc4e5b commit 18d286f

File tree

2 files changed

+101
-32
lines changed

2 files changed

+101
-32
lines changed

cache.go

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

33
import (
4+
"runtime"
45
"sync"
56
"time"
67

@@ -12,6 +13,15 @@ import (
1213
"github.com/Code-Hex/go-generics-cache/policy/simple"
1314
)
1415

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+
1525
// Interface is a common-cache interface.
1626
type Interface[K comparable, V any] interface {
1727
Get(key K) (value V, ok bool)
@@ -35,7 +45,7 @@ var (
3545
type Item[K comparable, V any] struct {
3646
Key K
3747
Value V
38-
Expiration time.Duration
48+
Expiration int64
3949
}
4050

4151
var nowFunc = time.Now
@@ -44,14 +54,23 @@ var nowFunc = time.Now
4454
type ItemOption func(*itemOptions)
4555

4656
type itemOptions struct {
47-
expiration time.Duration // default none
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
4867
}
4968

5069
// WithExpiration is an option to set expiration time for any items.
5170
// If the expiration is zero or negative value, it treats as w/o expiration.
5271
func WithExpiration(exp time.Duration) ItemOption {
5372
return func(o *itemOptions) {
54-
o.expiration = exp
73+
o.expiration = nowFunc().Add(exp).UnixNano()
5574
}
5675
}
5776

@@ -70,10 +89,11 @@ func newItem[K comparable, V any](key K, val V, opts ...ItemOption) *Item[K, V]
7089

7190
// Cache is a thread safe cache.
7291
type Cache[K comparable, V any] struct {
73-
cache Interface[K, *Item[K, V]]
74-
expirations map[K]chan struct{}
92+
cache Interface[K, *Item[K, V]]
93+
//expirations map[K]chan struct{}
7594
// mu is used to do lock in some method process.
76-
mu sync.Mutex
95+
mu sync.Mutex
96+
janitor *janitor
7797
}
7898

7999
// Option is an option for cache.
@@ -132,23 +152,77 @@ func New[K comparable, V any](opts ...Option[K, V]) *Cache[K, V] {
132152
for _, optFunc := range opts {
133153
optFunc(o)
134154
}
135-
return &Cache[K, V]{
136-
cache: o.cache,
137-
expirations: make(map[K]chan struct{}, 0),
155+
156+
cache := &Cache[K, V]{
157+
cache: o.cache,
158+
}
159+
160+
// @TODO change the ticker timer default value
161+
cache.runJanitor(cache, time.Minute)
162+
runtime.SetFinalizer(cache, cache.stopJanitor)
163+
164+
return cache
165+
}
166+
167+
func (_ *Cache[K, V]) stopJanitor(c *Cache[K, V]) {
168+
if c.janitor != nil {
169+
c.janitor.stop <- true
170+
}
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),
138181
}
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+
}()
139197
}
140198

141199
// Get looks up a key's value from the cache.
142200
func (c *Cache[K, V]) Get(key K) (value V, ok bool) {
143201
c.mu.Lock()
144202
defer c.mu.Unlock()
145203
item, ok := c.cache.Get(key)
204+
146205
if !ok {
147206
return
148207
}
208+
209+
// if is expired, delete is and return nil instead
210+
if item.Expiration > 0 && nowFunc().UnixNano() > item.Expiration {
211+
c.cache.Delete(key)
212+
return value, false
213+
}
214+
149215
return item.Value, true
150216
}
151217

218+
// DeleteExpired all expired items from the cache.
219+
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)
223+
}
224+
}
225+
152226
// Set sets a value to the cache with key. replacing any existing value.
153227
func (c *Cache[K, V]) Set(key K, val V, opts ...ItemOption) {
154228
c.mu.Lock()
@@ -159,30 +233,7 @@ func (c *Cache[K, V]) Set(key K, val V, opts ...ItemOption) {
159233
return
160234
}
161235

162-
if _, ok := c.cache.Get(key); ok {
163-
c.doneWatchExpiration(key)
164-
}
165-
166236
c.cache.Set(key, item)
167-
c.installExpirationWatcher(item.Key, item.Expiration)
168-
}
169-
170-
func (c *Cache[K, V]) installExpirationWatcher(key K, exp time.Duration) {
171-
done := make(chan struct{})
172-
c.expirations[key] = done
173-
go func() {
174-
select {
175-
case <-time.After(exp):
176-
c.Delete(key)
177-
case <-done:
178-
}
179-
}()
180-
}
181-
182-
func (c *Cache[K, V]) doneWatchExpiration(key K) {
183-
if ch, ok := c.expirations[key]; ok {
184-
close(ch)
185-
}
186237
}
187238

188239
// Keys returns the keys of the cache. the order is relied on algorithms.

cache_test.go

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

89
cache "github.com/Code-Hex/go-generics-cache"
910
"github.com/Code-Hex/go-generics-cache/policy/clock"
@@ -13,6 +14,23 @@ import (
1314
"github.com/Code-Hex/go-generics-cache/policy/mru"
1415
)
1516

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+
1634
func TestMultiThreadIncr(t *testing.T) {
1735
nc := cache.NewNumber[string, int]()
1836
nc.Set("counter", 0)

0 commit comments

Comments
 (0)