Skip to content

Commit d5857d5

Browse files
committed
Add in-memory cache implementation.
1 parent 9435245 commit d5857d5

File tree

2 files changed

+338
-0
lines changed

2 files changed

+338
-0
lines changed

cache/cache.go

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
package cache
2+
3+
import (
4+
"container/list"
5+
"sync"
6+
)
7+
8+
// Cache is a bounded-size in-memory cache of sized items with a configurable eviction policy
9+
type Cache interface {
10+
// Get retrieves items from the cache by key.
11+
// If an item for a particular key is not found, its position in the result will be nil.
12+
Get(keys ...string) []Item
13+
14+
// Put adds an item to the cache.
15+
Put(key string, item Item)
16+
17+
// Remove clears items with the given keys from the cache
18+
Remove(keys ...string)
19+
20+
// Size returns the size of all items currently in the cache.
21+
Size() uint64
22+
}
23+
24+
// Item is an item in a cache
25+
type Item interface {
26+
// Size returns the item's size, in bytes
27+
Size() uint64
28+
}
29+
30+
// A tuple tracking a cached item and a reference to its node in the eviction list
31+
type cached struct {
32+
item Item
33+
element *list.Element
34+
}
35+
36+
// Sets the provided list element on the cached item if it is not nil
37+
func (c *cached) setElementIfNotNil(element *list.Element) {
38+
if element != nil {
39+
c.element = element
40+
}
41+
}
42+
43+
// Private cache implementation
44+
type cache struct {
45+
sync.Mutex // Lock for synchronizing Get, Put, Remove
46+
cap uint64 // Capacity bound
47+
size uint64 // Cumulative size
48+
items map[string]*cached // Map from keys to cached items
49+
keyList *list.List // List of cached items in order of increasing evictability
50+
recordAdd func(key string) *list.Element // Function called to indicate that an item with the given key was added
51+
recordAccess func(key string) *list.Element // Function called to indicate that an item with the given key was accessed
52+
}
53+
54+
// CacheOption configures a cache.
55+
type CacheOption func(*cache)
56+
57+
// Capacity sets the fixed capacity bound on the cache, in bytes.
58+
// If not provided, default is 10MB.
59+
func Capacity(cap uint64) CacheOption {
60+
return func(c *cache) {
61+
c.cap = cap
62+
}
63+
}
64+
65+
// Policy is a cache eviction policy for use with the EvictionPolicy CacheOption.
66+
type Policy uint8
67+
68+
const (
69+
// LeastRecentlyAdded indicates a least-recently-added eviction policy.
70+
LeastRecentlyAdded Policy = iota
71+
// LeastRecentlyUsed indicates a least-recently-used eviction policy.
72+
LeastRecentlyUsed
73+
)
74+
75+
// EvictionPolicy sets the eviction policy to be used to make room for new items.
76+
// If not provided, default is LeastRecentlyUsed.
77+
func EvictionPolicy(policy Policy) CacheOption {
78+
return func(c *cache) {
79+
switch policy {
80+
case LeastRecentlyAdded:
81+
c.recordAccess = c.noop
82+
c.recordAdd = c.record
83+
case LeastRecentlyUsed:
84+
c.recordAccess = c.record
85+
c.recordAdd = c.noop
86+
}
87+
}
88+
}
89+
90+
// New returns a cache with the requested options configured.
91+
// The cache consumes memory bounded by a fixed capacity,
92+
// plus tracking overhead linear in the number of items.
93+
func New(options ...CacheOption) Cache {
94+
c := &cache{
95+
cap: 10 * 1024 * 1024, // Default capacity of 10MB
96+
keyList: list.New(),
97+
items: map[string]*cached{},
98+
}
99+
// Default LRU eviction policy
100+
EvictionPolicy(LeastRecentlyUsed)(c)
101+
102+
for _, option := range options {
103+
option(c)
104+
}
105+
106+
return c
107+
}
108+
109+
func (c *cache) Get(keys ...string) []Item {
110+
c.Lock()
111+
defer c.Unlock()
112+
113+
items := make([]Item, len(keys))
114+
for i, key := range keys {
115+
cached := c.items[key]
116+
if cached == nil {
117+
items[i] = nil
118+
} else {
119+
c.recordAccess(key)
120+
items[i] = cached.item
121+
}
122+
}
123+
124+
return items
125+
}
126+
127+
func (c *cache) Put(key string, item Item) {
128+
c.Lock()
129+
defer c.Unlock()
130+
131+
// Remove the item currently with this key (if any)
132+
c.remove(key)
133+
134+
// Make sure there's room to add this item
135+
c.ensureCapacity(item.Size())
136+
137+
// Actually add the new item
138+
cached := &cached{item: item}
139+
cached.setElementIfNotNil(c.recordAdd(key))
140+
cached.setElementIfNotNil(c.recordAccess(key))
141+
c.items[key] = cached
142+
c.size += item.Size()
143+
}
144+
145+
func (c *cache) Remove(keys ...string) {
146+
c.Lock()
147+
defer c.Unlock()
148+
149+
for _, key := range keys {
150+
c.remove(key)
151+
}
152+
}
153+
154+
func (c *cache) Size() uint64 {
155+
return c.size
156+
}
157+
158+
// Given the need to add some number of new bytes to the cache,
159+
// evict items according to the eviction policy until there is room.
160+
// The caller should hold the cache lock.
161+
func (c *cache) ensureCapacity(toAdd uint64) {
162+
mustRemove := int64(c.size+toAdd) - int64(c.cap)
163+
for mustRemove > 0 {
164+
key := c.keyList.Back().Value.(string)
165+
mustRemove -= int64(c.items[key].item.Size())
166+
c.remove(key)
167+
}
168+
}
169+
170+
// Remove the item associated with the given key.
171+
// The caller should hold the cache lock.
172+
func (c *cache) remove(key string) {
173+
if cached, ok := c.items[key]; ok {
174+
delete(c.items, key)
175+
c.size -= cached.item.Size()
176+
c.keyList.Remove(cached.element)
177+
}
178+
}
179+
180+
// A no-op function that does nothing for the provided key
181+
func (c *cache) noop(string) *list.Element { return nil }
182+
183+
// A function to record the given key and mark it as last to be evicted
184+
func (c *cache) record(key string) *list.Element {
185+
if item, ok := c.items[key]; ok {
186+
c.keyList.MoveToFront(item.element)
187+
return item.element
188+
}
189+
return c.keyList.PushFront(key)
190+
}

cache/cache_test.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package cache
2+
3+
import (
4+
"container/list"
5+
"testing"
6+
)
7+
8+
func TestCapacity(t *testing.T) {
9+
c := &cache{}
10+
Capacity(52)(c)
11+
if c.cap != 52 {
12+
t.Errorf("Capacity failed to set cap")
13+
}
14+
}
15+
16+
func TestEvictionPolicy(t *testing.T) {
17+
c := &cache{keyList: list.New()}
18+
EvictionPolicy(LeastRecentlyUsed)(c)
19+
if accessed, added := c.recordAccess("foo"), c.recordAdd("foo"); accessed == nil || added != nil {
20+
t.Errorf("EvictionPolicy failed to set LRU policy")
21+
}
22+
23+
c = &cache{keyList: list.New()}
24+
EvictionPolicy(LeastRecentlyAdded)(c)
25+
if accessed, added := c.recordAccess("foo"), c.recordAdd("foo"); accessed != nil || added == nil {
26+
t.Errorf("EvictionPolicy failed to set LRU policy")
27+
}
28+
}
29+
30+
func TestNew(t *testing.T) {
31+
optionApplied := false
32+
option := func(*cache) {
33+
optionApplied = true
34+
}
35+
36+
c := New(option).(*cache)
37+
38+
if c.cap != 10*1024*1024 {
39+
t.Errorf("Expected default cache capacity of %d", 10*1024*1024)
40+
}
41+
if c.size != 0 {
42+
t.Errorf("Expected initial size of zero")
43+
}
44+
if c.items == nil {
45+
t.Errorf("Expected items to be initialized")
46+
}
47+
if c.keyList == nil {
48+
t.Errorf("Expected keyList to be initialized")
49+
}
50+
if !optionApplied {
51+
t.Errorf("New did not apply its provided option")
52+
}
53+
if accessed, added := c.recordAccess("foo"), c.recordAdd("foo"); accessed == nil || added != nil {
54+
t.Errorf("Expected default LRU policy")
55+
}
56+
}
57+
58+
type testItem uint64
59+
60+
func (ti testItem) Size() uint64 {
61+
return uint64(ti)
62+
}
63+
64+
func TestPutGetRemoveSize(t *testing.T) {
65+
keys := []string{"foo", "bar", "baz"}
66+
testCases := []struct {
67+
label string
68+
cache Cache
69+
useCache func(c Cache)
70+
expectedSize uint64
71+
expectedItems []Item
72+
}{{
73+
label: "Items added, key doesn't exist",
74+
cache: New(),
75+
useCache: func(c Cache) {
76+
c.Put("foo", testItem(1))
77+
},
78+
expectedSize: 1,
79+
expectedItems: []Item{testItem(1), nil, nil},
80+
}, {
81+
label: "Items added, key exists",
82+
cache: New(),
83+
useCache: func(c Cache) {
84+
c.Put("foo", testItem(1))
85+
c.Put("foo", testItem(10))
86+
},
87+
expectedSize: 10,
88+
expectedItems: []Item{testItem(10), nil, nil},
89+
}, {
90+
label: "Items added, LRA eviction",
91+
cache: New(Capacity(2), EvictionPolicy(LeastRecentlyAdded)),
92+
useCache: func(c Cache) {
93+
c.Put("foo", testItem(1))
94+
c.Put("bar", testItem(1))
95+
c.Get("foo")
96+
c.Put("baz", testItem(1))
97+
},
98+
expectedSize: 2,
99+
expectedItems: []Item{nil, testItem(1), testItem(1)},
100+
}, {
101+
label: "Items added, LRU eviction",
102+
cache: New(Capacity(2), EvictionPolicy(LeastRecentlyUsed)),
103+
useCache: func(c Cache) {
104+
c.Put("foo", testItem(1))
105+
c.Put("bar", testItem(1))
106+
c.Get("foo")
107+
c.Put("baz", testItem(1))
108+
},
109+
expectedSize: 2,
110+
expectedItems: []Item{testItem(1), nil, testItem(1)},
111+
}, {
112+
label: "Items removed, key doesn't exist",
113+
cache: New(),
114+
useCache: func(c Cache) {
115+
c.Put("foo", testItem(1))
116+
c.Remove("baz")
117+
},
118+
expectedSize: 1,
119+
expectedItems: []Item{testItem(1), nil, nil},
120+
}, {
121+
label: "Items removed, key exists",
122+
cache: New(),
123+
useCache: func(c Cache) {
124+
c.Put("foo", testItem(1))
125+
c.Remove("foo")
126+
},
127+
expectedSize: 0,
128+
expectedItems: []Item{nil, nil, nil},
129+
}}
130+
131+
for _, testCase := range testCases {
132+
t.Log(testCase.label)
133+
testCase.useCache(testCase.cache)
134+
if testCase.cache.Size() != testCase.expectedSize {
135+
t.Errorf("Expected size of %d, got %d", testCase.expectedSize, testCase.cache.Size())
136+
}
137+
actual := testCase.cache.Get(keys...)
138+
if len(actual) != len(testCase.expectedItems) {
139+
t.Errorf("Expected to get %d items, got %d", len(testCase.expectedItems), len(actual))
140+
} else {
141+
for i, expectedItem := range testCase.expectedItems {
142+
if actual[i] != expectedItem {
143+
t.Errorf("Expected Get to return %v in position %d, got %v", expectedItem, i, actual[i])
144+
}
145+
}
146+
}
147+
}
148+
}

0 commit comments

Comments
 (0)