Skip to content

Commit f983da1

Browse files
committed
fixed cache item behavior
1 parent ade978f commit f983da1

File tree

7 files changed

+140
-184
lines changed

7 files changed

+140
-184
lines changed

cache.go

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,60 @@ package cache
22

33
import (
44
"sync"
5+
"time"
56
)
67

78
// Cache is a common-cache interface.
89
type Cache[K comparable, V any] interface {
910
Get(key K) (value V, ok bool)
10-
Set(key K, val V)
11+
Set(key K, val V, opts ...ItemOption)
12+
}
13+
14+
// Item is an item
15+
type Item[K comparable, V any] struct {
16+
Key K
17+
Value V
18+
Expiration time.Duration
19+
CreatedAt time.Time
20+
}
21+
22+
var nowFunc = time.Now
23+
24+
// HasExpired returns true if the item has expired.
25+
// If the item's expiration is zero value, returns false.
26+
func (i Item[K, T]) HasExpired() bool {
27+
if i.Expiration <= 0 {
28+
return false
29+
}
30+
return i.CreatedAt.Add(i.Expiration).Before(nowFunc())
31+
}
32+
33+
// ItemOption is an option for cache item.
34+
type ItemOption func(*itemOptions)
35+
36+
type itemOptions struct {
37+
expiration time.Duration // default none
38+
}
39+
40+
// WithExpiration is an option to set expiration time for any items.
41+
func WithExpiration(exp time.Duration) ItemOption {
42+
return func(o *itemOptions) {
43+
o.expiration = exp
44+
}
45+
}
46+
47+
// NewItem creates a new item with specified any options.
48+
func NewItem[K comparable, V any](key K, val V, opts ...ItemOption) *Item[K, V] {
49+
o := new(itemOptions)
50+
for _, optFunc := range opts {
51+
optFunc(o)
52+
}
53+
return &Item[K, V]{
54+
Key: key,
55+
Value: val,
56+
Expiration: o.expiration,
57+
CreatedAt: nowFunc(),
58+
}
1159
}
1260

1361
// NumberCache is a in-memory cache which is able to store only Number constraint.

cache_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cache_test
33
import (
44
"sync"
55
"testing"
6+
"time"
67

78
cache "github.com/Code-Hex/go-generics-cache"
89
"github.com/Code-Hex/go-generics-cache/simple"
@@ -49,3 +50,54 @@ func TestMultiThreadDecr(t *testing.T) {
4950
t.Errorf("want %v but got %v", 0, counter)
5051
}
5152
}
53+
54+
func TestHasExpired(t *testing.T) {
55+
cases := []struct {
56+
name string
57+
exp time.Duration
58+
createdAt time.Time
59+
current time.Time
60+
want bool
61+
}{
62+
// expiration == createdAt + exp
63+
{
64+
name: "item expiration is zero",
65+
want: false,
66+
},
67+
{
68+
name: "item expiration > current time",
69+
exp: time.Hour * 24,
70+
createdAt: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
71+
current: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
72+
want: false,
73+
},
74+
{
75+
name: "item expiration < current time",
76+
exp: time.Hour * 24,
77+
createdAt: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
78+
current: time.Date(2009, time.November, 12, 23, 0, 0, 0, time.UTC),
79+
want: true,
80+
},
81+
{
82+
name: "item expiration == current time",
83+
exp: time.Second,
84+
createdAt: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
85+
current: time.Date(2009, time.November, 10, 23, 0, 1, 0, time.UTC),
86+
want: false,
87+
},
88+
}
89+
for _, tc := range cases {
90+
t.Run(tc.name, func(t *testing.T) {
91+
reset := cache.SetNowFunc(tc.current)
92+
defer reset()
93+
94+
it := &cache.Item[int, int]{
95+
Expiration: tc.exp,
96+
CreatedAt: tc.createdAt,
97+
}
98+
if got := it.HasExpired(); tc.want != got {
99+
t.Fatalf("want %v, but got %v", tc.want, got)
100+
}
101+
})
102+
}
103+
}

export_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package cache
2+
3+
import "time"
4+
5+
func SetNowFunc(tm time.Time) (reset func()) {
6+
backup := nowFunc
7+
nowFunc = func() time.Time { return tm }
8+
return func() {
9+
nowFunc = backup
10+
}
11+
}

lru/lru.go

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,6 @@ type Cache[K comparable, V any] struct {
1717

1818
var _ cache.Cache[interface{}, any] = (*Cache[interface{}, any])(nil)
1919

20-
type item[K comparable, V any] struct {
21-
Key K
22-
Value V
23-
}
24-
2520
// NewCache creates a new LRU cache whose capacity is the default size (128).
2621
func NewCache[K comparable, V any]() *Cache[K, V] {
2722
return NewCacheWithCap[K, V](128)
@@ -40,30 +35,33 @@ func NewCacheWithCap[K comparable, V any](cap int) *Cache[K, V] {
4035
func (c *Cache[K, V]) Get(key K) (zero V, _ bool) {
4136
c.mu.RLock()
4237
defer c.mu.RUnlock()
43-
if e, ok := c.items[key]; ok {
44-
// updates cache order
45-
c.list.MoveToFront(e)
46-
return e.Value.(*item[K, V]).Value, true
38+
e, ok := c.items[key]
39+
if !ok {
40+
return
41+
}
42+
item := e.Value.(*cache.Item[K, V])
43+
if item.HasExpired() {
44+
return
4745
}
48-
return
46+
// updates cache order
47+
c.list.MoveToFront(e)
48+
return item.Value, true
4949
}
5050

5151
// Set sets a value to the cache with key. replacing any existing value.
52-
func (c *Cache[K, V]) Set(key K, val V) {
52+
func (c *Cache[K, V]) Set(key K, val V, opts ...cache.ItemOption) {
5353
c.mu.Lock()
5454
defer c.mu.Unlock()
5555

5656
if e, ok := c.items[key]; ok {
5757
// updates cache order
5858
c.list.MoveToFront(e)
59-
e.Value.(*item[K, V]).Value = val
59+
e.Value.(*cache.Item[K, V]).Value = val
6060
return
6161
}
6262

63-
e := c.list.PushFront(&item[K, V]{
64-
Key: key,
65-
Value: val,
66-
})
63+
item := cache.NewItem(key, val, opts...)
64+
e := c.list.PushFront(item)
6765
c.items[key] = e
6866

6967
if c.list.Len() > c.cap {
@@ -77,7 +75,7 @@ func (c *Cache[K, V]) Keys() []K {
7775
defer c.mu.RUnlock()
7876
keys := make([]K, 0, len(c.items))
7977
for ent := c.list.Back(); ent != nil; ent = ent.Prev() {
80-
keys = append(keys, ent.Value.(*item[K, V]).Key)
78+
keys = append(keys, ent.Value.(*cache.Item[K, V]).Key)
8179
}
8280
return keys
8381
}
@@ -113,6 +111,6 @@ func (c *Cache[K, V]) deleteOldest() {
113111

114112
func (c *Cache[K, V]) delete(e *list.Element) {
115113
c.list.Remove(e)
116-
item := e.Value.(*item[K, V])
114+
item := e.Value.(*cache.Item[K, V])
117115
delete(c.items, item.Key)
118116
}

simple/example_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ import (
55
"sort"
66
"time"
77

8+
cache "github.com/Code-Hex/go-generics-cache"
89
"github.com/Code-Hex/go-generics-cache/simple"
910
)
1011

1112
func ExampleCache() {
12-
c := simple.NewCache[string, int](simple.WithExpiration(time.Hour))
13-
c.Set("a", 1)
13+
c := simple.NewCache[string, int]()
14+
c.Set("a", 1, cache.WithExpiration(time.Hour))
1415
c.Set("b", 2)
1516
av, aok := c.Get("a")
1617
bv, bok := c.Get("b")

simple/simple.go

Lines changed: 9 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,119 +1,48 @@
11
package simple
22

33
import (
4-
"errors"
5-
"fmt"
64
"sync"
7-
"time"
85

96
cache "github.com/Code-Hex/go-generics-cache"
107
)
118

12-
var (
13-
// ErrNotFound is an error which indicate an item is not found.
14-
ErrNotFound = errors.New("not found item")
15-
16-
// ErrExpired is an error which indicate an item is expired.
17-
ErrExpired = errors.New("expired item")
18-
)
19-
20-
// Item is an item
21-
type Item[T any] struct {
22-
Value T
23-
Expiration time.Duration
24-
CreatedAt time.Time
25-
}
26-
27-
var nowFunc = time.Now
28-
29-
// HasExpired returns true if the item has expired.
30-
// If the item's expiration is zero value, returns false.
31-
func (i Item[T]) HasExpired() bool {
32-
if i.Expiration <= 0 {
33-
return false
34-
}
35-
return i.CreatedAt.Add(i.Expiration).Before(nowFunc())
36-
}
37-
389
// Cache is a simple cache has no clear priority for evict cache.
3910
type Cache[K comparable, V any] struct {
40-
items map[K]*Item[V]
41-
options *options
42-
mu sync.RWMutex
11+
items map[K]*cache.Item[K, V]
12+
mu sync.RWMutex
4313
}
4414

4515
var _ cache.Cache[interface{}, any] = (*Cache[interface{}, any])(nil)
4616

4717
// NewCache creates a new cache.
48-
func NewCache[K comparable, V any](opts ...Option) *Cache[K, V] {
49-
o := new(options)
50-
for _, optFunc := range opts {
51-
optFunc(o)
52-
}
18+
func NewCache[K comparable, V any]() *Cache[K, V] {
5319
return &Cache[K, V]{
54-
items: make(map[K]*Item[V]),
55-
options: o,
56-
}
57-
}
58-
59-
// Option is an option for cache.
60-
type Option func(o *options)
61-
62-
type options struct {
63-
expiration time.Duration // default none
64-
}
65-
66-
// WithExpiration is an option to set expiration time for any items.
67-
func WithExpiration(exp time.Duration) Option {
68-
return func(o *options) {
69-
o.expiration = exp
20+
items: make(map[K]*cache.Item[K, V]),
7021
}
7122
}
7223

7324
// Set sets any item to the cache. replacing any existing item.
7425
// The default item never expires.
75-
func (c *Cache[K, V]) Set(k K, v V) {
76-
item := &Item[V]{
77-
Value: v,
78-
Expiration: c.options.expiration,
79-
CreatedAt: nowFunc(),
80-
}
81-
c.SetItem(k, item)
82-
}
83-
84-
// SetItem sets any item to the cache. replacing any existing item.
85-
// The default item never expires.
86-
func (c *Cache[K, V]) SetItem(k K, v *Item[V]) {
26+
func (c *Cache[K, V]) Set(k K, v V, opts ...cache.ItemOption) {
8727
c.mu.Lock()
88-
c.items[k] = v
28+
c.items[k] = cache.NewItem(k, v, opts...)
8929
c.mu.Unlock()
9030
}
9131

9232
// Get gets an item from the cache.
9333
// Returns the item or zero value, and a bool indicating whether the key was found.
9434
func (c *Cache[K, V]) Get(k K) (val V, ok bool) {
95-
item, err := c.GetItem(k)
96-
if err != nil {
97-
return
98-
}
99-
return item.Value, true
100-
}
101-
102-
// GetItem gets an item from the cache.
103-
// Returns an error if the item was not found or expired. If there is no error, the
104-
// incremented value is returned.
105-
func (c *Cache[K, V]) GetItem(k K) (val *Item[V], _ error) {
10635
c.mu.RLock()
10736
defer c.mu.RUnlock()
10837

10938
got, found := c.items[k]
11039
if !found {
111-
return nil, fmt.Errorf("key[%v]: %w", k, ErrNotFound)
40+
return
11241
}
11342
if got.HasExpired() {
114-
return nil, fmt.Errorf("key[%v]: %w", k, ErrExpired)
43+
return
11544
}
116-
return got, nil
45+
return got.Value, true
11746
}
11847

11948
// Keys returns cache keys. the order is random.

0 commit comments

Comments
 (0)