Skip to content

Commit 61d859c

Browse files
authored
Merge pull request #6 from Code-Hex/fix/item-behavior
fixed item behavior
2 parents ade978f + 4e9b01a commit 61d859c

File tree

11 files changed

+338
-222
lines changed

11 files changed

+338
-222
lines changed

cache.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,63 @@ 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+
Keys() []K
13+
Delete(key K)
14+
Contains(key K) bool
15+
}
16+
17+
// Item is an item
18+
type Item[K comparable, V any] struct {
19+
Key K
20+
Value V
21+
Expiration time.Duration
22+
CreatedAt time.Time
23+
}
24+
25+
var nowFunc = time.Now
26+
27+
// HasExpired returns true if the item has expired.
28+
// If the item's expiration is zero value, returns false.
29+
func (i Item[K, T]) HasExpired() bool {
30+
if i.Expiration <= 0 {
31+
return false
32+
}
33+
return i.CreatedAt.Add(i.Expiration).Before(nowFunc())
34+
}
35+
36+
// ItemOption is an option for cache item.
37+
type ItemOption func(*itemOptions)
38+
39+
type itemOptions struct {
40+
expiration time.Duration // default none
41+
}
42+
43+
// WithExpiration is an option to set expiration time for any items.
44+
func WithExpiration(exp time.Duration) ItemOption {
45+
return func(o *itemOptions) {
46+
o.expiration = exp
47+
}
48+
}
49+
50+
// NewItem creates a new item with specified any options.
51+
func NewItem[K comparable, V any](key K, val V, opts ...ItemOption) *Item[K, V] {
52+
o := new(itemOptions)
53+
for _, optFunc := range opts {
54+
optFunc(o)
55+
}
56+
return &Item[K, V]{
57+
Key: key,
58+
Value: val,
59+
Expiration: o.expiration,
60+
CreatedAt: nowFunc(),
61+
}
1162
}
1263

1364
// 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+
}

example_test.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,26 @@ package cache_test
22

33
import (
44
"fmt"
5+
"time"
56

67
cache "github.com/Code-Hex/go-generics-cache"
78
"github.com/Code-Hex/go-generics-cache/simple"
89
)
910

1011
func ExampleNumberCache() {
11-
c := cache.NewNumber[string, int](simple.NewCache[string, int]())
12-
c.Set("a", 1)
13-
c.Set("b", 2)
14-
av := c.Increment("a", 1)
15-
gota, aok := c.Get("a")
12+
c := simple.NewCache[string, int]()
13+
nc := cache.NewNumber[string, int](c)
14+
nc.Set("a", 1)
15+
nc.Set("b", 2, cache.WithExpiration(time.Minute))
16+
av := nc.Increment("a", 1)
17+
gota, aok := nc.Get("a")
1618

17-
bv := c.Decrement("b", 1)
18-
gotb, bok := c.Get("b")
19+
bv := nc.Decrement("b", 1)
20+
gotb, bok := nc.Get("b")
1921

2022
// not set keys
21-
cv := c.Increment("c", 100)
22-
dv := c.Decrement("d", 100)
23+
cv := nc.Increment("c", 100)
24+
dv := nc.Decrement("d", 100)
2325
fmt.Println(av, gota, aok)
2426
fmt.Println(bv, gotb, bok)
2527
fmt.Println(cv)

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: 24 additions & 21 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)
@@ -32,38 +27,41 @@ func NewCacheWithCap[K comparable, V any](cap int) *Cache[K, V] {
3227
return &Cache[K, V]{
3328
cap: cap,
3429
list: list.New(),
35-
items: make(map[K]*list.Element),
30+
items: make(map[K]*list.Element, cap),
3631
}
3732
}
3833

3934
// Get looks up a key's value from the cache.
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,8 @@ 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+
item := ent.Value.(*cache.Item[K, V])
79+
keys = append(keys, item.Key)
8180
}
8281
return keys
8382
}
@@ -102,8 +101,12 @@ func (c *Cache[K, V]) Delete(key K) {
102101
func (c *Cache[K, V]) Contains(key K) bool {
103102
c.mu.RLock()
104103
defer c.mu.RUnlock()
105-
_, ok := c.items[key]
106-
return ok
104+
e, ok := c.items[key]
105+
if !ok {
106+
return false
107+
}
108+
item := e.Value.(*cache.Item[K, V])
109+
return !item.HasExpired()
107110
}
108111

109112
func (c *Cache[K, V]) deleteOldest() {
@@ -113,6 +116,6 @@ func (c *Cache[K, V]) deleteOldest() {
113116

114117
func (c *Cache[K, V]) delete(e *list.Element) {
115118
c.list.Remove(e)
116-
item := e.Value.(*item[K, V])
119+
item := e.Value.(*cache.Item[K, V])
117120
delete(c.items, item.Key)
118121
}

lru/lru_internal_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package lru
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
cache "github.com/Code-Hex/go-generics-cache"
8+
)
9+
10+
func TestContains(t *testing.T) {
11+
t.Run("without expiration", func(t *testing.T) {
12+
cache := NewCache[string, int]()
13+
cache.Set("foo", 1)
14+
cache.Set("bar", 2)
15+
cache.Set("baz", 3)
16+
for _, key := range []string{
17+
"foo",
18+
"bar",
19+
"baz",
20+
} {
21+
if !cache.Contains(key) {
22+
t.Errorf("not found: %s", key)
23+
}
24+
}
25+
if cache.Contains("not found") {
26+
t.Errorf("found")
27+
}
28+
})
29+
30+
t.Run("with expiration", func(t *testing.T) {
31+
c := NewCache[string, int]()
32+
key := "foo"
33+
exp := time.Hour
34+
c.Set(key, 1, cache.WithExpiration(exp))
35+
// modify directly
36+
e, ok := c.items[key]
37+
if !ok {
38+
t.Fatal("unexpected not found key")
39+
}
40+
item := e.Value.(*cache.Item[string, int])
41+
item.CreatedAt = time.Now().Add(-2 * exp)
42+
43+
if c.Contains(key) {
44+
t.Errorf("found")
45+
}
46+
})
47+
}
48+
49+
func TestGet(t *testing.T) {
50+
c := NewCache[string, int]()
51+
key := "foo"
52+
exp := time.Hour
53+
c.Set(key, 1, cache.WithExpiration(exp))
54+
_, ok := c.Get(key)
55+
if !ok {
56+
t.Fatal("unexpected not found")
57+
}
58+
// modify directly
59+
e, ok := c.items[key]
60+
if !ok {
61+
t.Fatal("unexpected not found key")
62+
}
63+
item := e.Value.(*cache.Item[string, int])
64+
item.CreatedAt = time.Now().Add(-2 * exp)
65+
_, ok2 := c.Get(key)
66+
if ok2 {
67+
t.Fatal("unexpected found (expired)")
68+
}
69+
}

lru/lru_test.go

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -43,22 +43,6 @@ func TestSet(t *testing.T) {
4343
}
4444
}
4545

46-
func TestContains(t *testing.T) {
47-
cache := lru.NewCache[string, int]()
48-
cache.Set("foo", 1)
49-
cache.Set("bar", 2)
50-
cache.Set("baz", 3)
51-
for _, key := range []string{
52-
"foo",
53-
"bar",
54-
"baz",
55-
} {
56-
if !cache.Contains(key) {
57-
t.Errorf("not found: %s", key)
58-
}
59-
}
60-
}
61-
6246
func TestDelete(t *testing.T) {
6347
cache := lru.NewCacheWithCap[string, int](1)
6448
cache.Set("foo", 1)
@@ -78,5 +62,4 @@ func TestDelete(t *testing.T) {
7862
if _, ok := cache.Get("foo"); ok {
7963
t.Fatalf("invalid get after deleted %v", ok)
8064
}
81-
8265
}

0 commit comments

Comments
 (0)