Skip to content

Commit 8144927

Browse files
authored
Merge pull request #10 from Code-Hex/refactor/cache
2 parents 2d72735 + ea5768c commit 8144927

16 files changed

+342
-498
lines changed

README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,21 @@ import (
4343
"time"
4444

4545
cache "github.com/Code-Hex/go-generics-cache"
46-
"github.com/Code-Hex/go-generics-cache/simple"
4746
)
4847

4948
func main() {
50-
// Create a simple cache. key as string, value as int.
51-
simpleCache := simple.NewCache[string, int]()
49+
// use simple cache algorithm without options.
50+
c := cache.New[string, int]()
51+
c.Set("a", 1)
52+
gota, aok := c.Get("a")
53+
gotb, bok := c.Get("b")
54+
fmt.Println(gota, aok) // 1 true
55+
fmt.Println(gotb, bok) // 0 false
5256

5357
// Create a cache for Number constraint. key as string, value as int.
54-
nc := cache.NewNumber[string, int](simpleCache)
58+
nc := cache.NewNumber[string, int]()
5559
nc.Set("age", 26, cache.WithExpiration(time.Hour))
5660

57-
// This will be compile error, because string is not satisfied cache.Number constraint.
58-
// nc := cache.NewNumber[string, string](simpleCache)
59-
6061
incremented := nc.Increment("age", 1)
6162
fmt.Println(incremented) // 27
6263

cache.go

Lines changed: 142 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,45 +3,35 @@ package cache
33
import (
44
"sync"
55
"time"
6+
7+
"github.com/Code-Hex/go-generics-cache/lfu"
8+
"github.com/Code-Hex/go-generics-cache/lru"
9+
"github.com/Code-Hex/go-generics-cache/simple"
610
)
711

8-
// Cache is a common-cache interface.
9-
type Cache[K comparable, V any] interface {
12+
// Interface is a common-cache interface.
13+
type Interface[K comparable, V any] interface {
1014
Get(key K) (value V, ok bool)
11-
Set(key K, val V, opts ...ItemOption)
15+
Set(key K, val V)
1216
Keys() []K
1317
Delete(key K)
14-
Contains(key K) bool
1518
}
1619

20+
var (
21+
_ Interface[any, any] = (*simple.Cache[any, any])(nil)
22+
_ Interface[any, any] = (*lru.Cache[any, any])(nil)
23+
_ Interface[any, any] = (*lfu.Cache[any, any])(nil)
24+
)
25+
1726
// Item is an item
1827
type Item[K comparable, V any] struct {
19-
Key K
20-
Value V
21-
ReferenceCount int
22-
Expiration time.Duration
23-
CreatedAt time.Time
24-
ReferencedAt time.Time
25-
}
26-
27-
// Referenced increments a reference counter and updates `ReferencedAt`
28-
// to current time.
29-
func (i *Item[K, V]) Referenced() {
30-
i.ReferenceCount++
31-
i.ReferencedAt = nowFunc()
28+
Key K
29+
Value V
30+
Expiration time.Duration
3231
}
3332

3433
var nowFunc = time.Now
3534

36-
// HasExpired returns true if the item has expired.
37-
// If the item's expiration is zero value, returns false.
38-
func (i Item[K, T]) HasExpired() bool {
39-
if i.Expiration <= 0 {
40-
return false
41-
}
42-
return i.CreatedAt.Add(i.Expiration).Before(nowFunc())
43-
}
44-
4535
// ItemOption is an option for cache item.
4636
type ItemOption func(*itemOptions)
4737

@@ -50,42 +40,155 @@ type itemOptions struct {
5040
}
5141

5242
// WithExpiration is an option to set expiration time for any items.
43+
// If the expiration is zero or negative value, it treats as w/o expiration.
5344
func WithExpiration(exp time.Duration) ItemOption {
5445
return func(o *itemOptions) {
5546
o.expiration = exp
5647
}
5748
}
5849

59-
// NewItem creates a new item with specified any options.
60-
func NewItem[K comparable, V any](key K, val V, opts ...ItemOption) *Item[K, V] {
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] {
6152
o := new(itemOptions)
6253
for _, optFunc := range opts {
6354
optFunc(o)
6455
}
65-
now := nowFunc()
6656
return &Item[K, V]{
67-
Key: key,
68-
Value: val,
69-
ReferenceCount: 1,
70-
Expiration: o.expiration,
71-
CreatedAt: now,
72-
ReferencedAt: now,
57+
Key: key,
58+
Value: val,
59+
Expiration: o.expiration,
60+
}
61+
}
62+
63+
// Cache is a cache.
64+
type Cache[K comparable, V any] struct {
65+
cache Interface[K, *Item[K, V]]
66+
expirations map[K]chan struct{}
67+
// mu is used to do lock in some method process.
68+
mu sync.RWMutex
69+
}
70+
71+
// Option is an option for cache.
72+
type Option[K comparable, V any] func(*options[K, V])
73+
74+
type options[K comparable, V any] struct {
75+
cache Interface[K, *Item[K, V]]
76+
}
77+
78+
func newOptions[K comparable, V any]() *options[K, V] {
79+
return &options[K, V]{
80+
cache: simple.NewCache[K, *Item[K, V]](),
81+
}
82+
}
83+
84+
// AsLRU is an option to make a new Cache as LRU algorithm.
85+
func AsLRU[K comparable, V any](opts ...lru.Option) Option[K, V] {
86+
return func(o *options[K, V]) {
87+
o.cache = lru.NewCache[K, *Item[K, V]](opts...)
88+
}
89+
}
90+
91+
// AsLFU is an option to make a new Cache as LFU algorithm.
92+
func AsLFU[K comparable, V any](opts ...lfu.Option) Option[K, V] {
93+
return func(o *options[K, V]) {
94+
o.cache = lfu.NewCache[K, *Item[K, V]](opts...)
95+
}
96+
}
97+
98+
// New creates a new Cache.
99+
func New[K comparable, V any](opts ...Option[K, V]) *Cache[K, V] {
100+
o := newOptions[K, V]()
101+
for _, optFunc := range opts {
102+
optFunc(o)
103+
}
104+
return &Cache[K, V]{
105+
cache: o.cache,
106+
expirations: make(map[K]chan struct{}, 0),
107+
}
108+
}
109+
110+
// Get looks up a key's value from the cache.
111+
func (c *Cache[K, V]) Get(key K) (value V, ok bool) {
112+
c.mu.RLock()
113+
defer c.mu.RUnlock()
114+
item, ok := c.cache.Get(key)
115+
if !ok {
116+
return
117+
}
118+
return item.Value, true
119+
}
120+
121+
// Set sets a value to the cache with key. replacing any existing value.
122+
func (c *Cache[K, V]) Set(key K, val V, opts ...ItemOption) {
123+
c.mu.Lock()
124+
defer c.mu.Unlock()
125+
item := newItem(key, val, opts...)
126+
if item.Expiration <= 0 {
127+
c.cache.Set(key, item)
128+
return
129+
}
130+
131+
if _, ok := c.cache.Get(key); ok {
132+
c.doneWatchExpiration(key)
73133
}
134+
135+
c.cache.Set(key, item)
136+
c.installExpirationWatcher(item.Key, item.Expiration)
137+
}
138+
139+
func (c *Cache[K, V]) installExpirationWatcher(key K, exp time.Duration) {
140+
done := make(chan struct{})
141+
c.expirations[key] = done
142+
go func() {
143+
select {
144+
case <-time.After(exp):
145+
c.Delete(key)
146+
case <-done:
147+
}
148+
}()
149+
}
150+
151+
func (c *Cache[K, V]) doneWatchExpiration(key K) {
152+
if ch, ok := c.expirations[key]; ok {
153+
close(ch)
154+
}
155+
}
156+
157+
// Keys returns the keys of the cache. the order is relied on algorithms.
158+
func (c *Cache[K, V]) Keys() []K {
159+
c.mu.RLock()
160+
defer c.mu.RUnlock()
161+
return c.cache.Keys()
162+
}
163+
164+
// Delete deletes the item with provided key from the cache.
165+
func (c *Cache[K, V]) Delete(key K) {
166+
c.mu.Lock()
167+
defer c.mu.Unlock()
168+
c.cache.Delete(key)
169+
}
170+
171+
// Contains reports whether key is within cache.
172+
func (c *Cache[K, V]) Contains(key K) bool {
173+
c.mu.RLock()
174+
defer c.mu.RUnlock()
175+
_, ok := c.cache.Get(key)
176+
return ok
74177
}
75178

76179
// NumberCache is a in-memory cache which is able to store only Number constraint.
77180
type NumberCache[K comparable, V Number] struct {
78-
Cache[K, V]
181+
*Cache[K, V]
79182
// nmu is used to do lock in Increment/Decrement process.
80-
// Note that this must be here as a separate mutex because mu in Cache struct is Locked in GetItem,
183+
// Note that this must be here as a separate mutex because mu in Cache struct is Locked in Get,
81184
// and if we call mu.Lock in Increment/Decrement, it will cause deadlock.
82185
nmu sync.Mutex
83186
}
84187

85188
// NewNumber creates a new cache for Number constraint.
86-
func NewNumber[K comparable, V Number](baseCache Cache[K, V]) *NumberCache[K, V] {
189+
func NewNumber[K comparable, V Number](opts ...Option[K, V]) *NumberCache[K, V] {
87190
return &NumberCache[K, V]{
88-
Cache: baseCache,
191+
Cache: New(opts...),
89192
}
90193
}
91194

cache_test.go

Lines changed: 2 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,12 @@ package cache_test
33
import (
44
"sync"
55
"testing"
6-
"time"
76

87
cache "github.com/Code-Hex/go-generics-cache"
9-
"github.com/Code-Hex/go-generics-cache/simple"
108
)
119

1210
func TestMultiThreadIncr(t *testing.T) {
13-
nc := cache.NewNumber[string, int](simple.NewCache[string, int]())
11+
nc := cache.NewNumber[string, int]()
1412
nc.Set("counter", 0)
1513

1614
var wg sync.WaitGroup
@@ -31,7 +29,7 @@ func TestMultiThreadIncr(t *testing.T) {
3129
}
3230

3331
func TestMultiThreadDecr(t *testing.T) {
34-
nc := cache.NewNumber[string, int](simple.NewCache[string, int]())
32+
nc := cache.NewNumber[string, int]()
3533
nc.Set("counter", 100)
3634

3735
var wg sync.WaitGroup
@@ -50,82 +48,3 @@ func TestMultiThreadDecr(t *testing.T) {
5048
t.Errorf("want %v but got %v", 0, counter)
5149
}
5250
}
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-
}
104-
105-
func TestReferenced(t *testing.T) {
106-
tm := time.Now()
107-
reset := cache.SetNowFunc(tm)
108-
109-
item := cache.NewItem("hello", "world")
110-
if item.ReferenceCount != 1 {
111-
t.Errorf("want ref count is 1 but got %d", item.ReferenceCount)
112-
}
113-
if !item.ReferencedAt.Equal(tm) {
114-
t.Errorf("unexpected referenced_at got %v", item.ReferencedAt)
115-
}
116-
117-
reset()
118-
// update referenced_at
119-
wantReferencedAt := tm.Add(time.Hour)
120-
reset = cache.SetNowFunc(wantReferencedAt)
121-
defer reset()
122-
123-
item.Referenced()
124-
125-
if item.ReferenceCount != 2 {
126-
t.Errorf("want ref count is 2 but got %d", item.ReferenceCount)
127-
}
128-
if !item.ReferencedAt.Equal(wantReferencedAt) {
129-
t.Errorf("unexpected referenced_at got %v", item.ReferencedAt)
130-
}
131-
}

0 commit comments

Comments
 (0)