Skip to content

Commit bb7e64f

Browse files
committed
Update cache for badges
1 parent 6516a53 commit bb7e64f

File tree

3 files changed

+159
-57
lines changed

3 files changed

+159
-57
lines changed

cmd/badges/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import (
2020
)
2121

2222
var uniqueCode = "unique_code"
23-
var cache = NewSimpleCache(1000)
23+
var cache = NewSimpleCache(1000, 86400)
2424
var countingSemaphore = make(chan bool, 1)
2525

2626
func main() {

cmd/badges/simplecache.go

Lines changed: 105 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -9,101 +9,153 @@ import (
99
type cacheEntry struct {
1010
entry []byte
1111
hits int
12+
age int64
1213
}
1314

1415
type SimpleCache struct {
15-
maxItems int
16-
items map[string]cacheEntry
17-
lock sync.Mutex
16+
maxItems int
17+
items map[string]cacheEntry
18+
lock sync.Mutex
19+
getUnix func() int64
20+
ageOutTimeSeconds int64
1821
}
1922

20-
func NewSimpleCache(maxItems int) *SimpleCache {
23+
func NewSimpleCache(maxItems int, ageOutTimeSeconds int64) *SimpleCache {
2124
simpleCache := SimpleCache{
2225
maxItems: maxItems,
2326
items: map[string]cacheEntry{},
2427
lock: sync.Mutex{},
28+
getUnix: func() int64 {
29+
return time.Now().Unix()
30+
},
31+
ageOutTimeSeconds: ageOutTimeSeconds,
2532
}
2633
simpleCache.runAgeItems()
2734
return &simpleCache
2835
}
2936

30-
func (cache *SimpleCache) runAgeItems() {
37+
func (c *SimpleCache) runAgeItems() {
3138
go func() {
32-
for {
33-
// maps are randomly ordered, so only decrementing 50 at a time should be acceptable
34-
count := 50
35-
cache.lock.Lock()
36-
for k, v := range cache.items {
37-
if v.hits > 0 {
38-
v.hits--
39-
cache.items[k] = v
40-
}
41-
count--
42-
if count <= 0 {
43-
break
44-
}
45-
}
46-
cache.lock.Unlock()
47-
time.Sleep(10 * time.Second)
48-
}
39+
time.Sleep(10 * time.Second)
40+
c.adjustLfu()
41+
c.ageOut()
4942
}()
5043
}
5144

52-
func (cache *SimpleCache) Add(cacheKey string, entry []byte) {
53-
cache.expireItems()
45+
func (c *SimpleCache) adjustLfu() {
46+
c.lock.Lock()
47+
defer c.lock.Unlock()
48+
49+
// maps are randomly ordered, so only decrementing 50 at a time should be acceptable
50+
count := 50
5451

55-
cache.lock.Lock()
56-
defer cache.lock.Unlock()
52+
for k, v := range c.items {
53+
if v.hits > 0 {
54+
v.hits--
55+
c.items[k] = v
56+
}
57+
count--
58+
if count <= 0 {
59+
break
60+
}
61+
}
62+
}
5763

58-
cache.items[cacheKey] = cacheEntry{
64+
func (c *SimpleCache) Add(cacheKey string, entry []byte) {
65+
c.evictItems()
66+
67+
c.lock.Lock()
68+
defer c.lock.Unlock()
69+
70+
c.items[cacheKey] = cacheEntry{
5971
entry: entry,
6072
hits: 1,
73+
age: c.getUnix(),
6174
}
6275
}
6376

64-
func (cache *SimpleCache) Get(cacheKey string) ([]byte, bool) {
65-
cache.lock.Lock()
66-
defer cache.lock.Unlock()
77+
func (c *SimpleCache) Get(cacheKey string) ([]byte, bool) {
78+
c.lock.Lock()
79+
defer c.lock.Unlock()
6780

68-
item, ok := cache.items[cacheKey]
81+
item, ok := c.items[cacheKey]
6982

7083
if ok {
7184
if item.hits < 100 {
7285
item.hits++
7386
}
74-
cache.items[cacheKey] = item
87+
c.items[cacheKey] = item
7588
return item.entry, true
7689
}
7790

7891
return nil, false
7992
}
8093

81-
// ExpireItems is called before any insert operation because we need to ensure we have less than
94+
// evictItems is called before any insert operation because we need to ensure we have less than
8295
// the total number of items
83-
func (cache *SimpleCache) expireItems() {
84-
cache.lock.Lock()
85-
defer cache.lock.Unlock()
96+
func (c *SimpleCache) evictItems() {
97+
c.lock.Lock()
98+
defer c.lock.Unlock()
99+
100+
// insert process only needs to expire if we have too much
101+
// as such if we haven't hit the limit return
102+
if len(c.items) < c.maxItems {
103+
return
104+
}
86105

87106
count := 10
88-
if len(cache.items) >= cache.maxItems {
89-
lfuKey := ""
90-
lfuLowestCount := math.MaxInt
91107

92-
for k, v := range cache.items {
93-
v.hits--
94-
cache.items[k] = v
95-
if v.hits < lfuLowestCount {
96-
lfuKey = k
97-
lfuLowestCount = v.hits
98-
}
99-
100-
// we only want to process X random elements so we don't spin forever
101-
count--
102-
if count <= 0 {
103-
break
104-
}
108+
lfuKey := ""
109+
lfuLowestCount := math.MaxInt
110+
111+
for k, v := range c.items {
112+
v.hits--
113+
c.items[k] = v
114+
if v.hits < lfuLowestCount {
115+
lfuKey = k
116+
lfuLowestCount = v.hits
105117
}
106118

107-
delete(cache.items, lfuKey)
119+
// we only want to process X random elements so we don't spin forever
120+
// however we also exit if the count is <= 0
121+
count--
122+
if count <= 0 || lfuLowestCount <= 0 {
123+
break
124+
}
125+
}
126+
127+
delete(c.items, lfuKey)
128+
}
129+
130+
// ageOut is called on a schedule to evict the oldest entry so long as
131+
// its older than the configured cache time
132+
func (c *SimpleCache) ageOut() {
133+
// we also want to age out things eventually to avoid https://github.com/boyter/scc/discussions/435
134+
// as such loop though and the first one that's older than a day with 0 hits is removed
135+
136+
c.lock.Lock()
137+
defer c.lock.Unlock()
138+
139+
count := 10
140+
lfuKey := ""
141+
lfuOldest := int64(math.MaxInt)
142+
143+
// maps are un-ordered so this is acceptable
144+
for k, v := range c.items {
145+
c.items[k] = v
146+
if v.age < lfuOldest {
147+
lfuKey = k
148+
lfuOldest = v.age
149+
}
150+
151+
count--
152+
if count <= 0 {
153+
break
154+
}
155+
}
156+
157+
// evict the oldest but only if its older than it should be
158+
if lfuOldest <= c.getUnix()-c.ageOutTimeSeconds {
159+
delete(c.items, lfuKey)
108160
}
109161
}

cmd/badges/simplecache_test.go

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package main
22

33
import (
44
"fmt"
5+
"sync"
56
"testing"
7+
"time"
68
)
79

810
func TestSimpleCache_Add(t *testing.T) {
9-
simpleCache := NewSimpleCache(5)
11+
simpleCache := NewSimpleCache(5, 60)
1012

1113
for i := 0; i < 5; i++ {
1214
simpleCache.Add(fmt.Sprintf("%d", i), []byte{})
@@ -22,7 +24,7 @@ func TestSimpleCache_Add(t *testing.T) {
2224
}
2325

2426
func TestSimpleCache_Multiple(t *testing.T) {
25-
simpleCache := NewSimpleCache(10)
27+
simpleCache := NewSimpleCache(10, 60)
2628

2729
for i := 0; i < 500; i++ {
2830
simpleCache.Add(fmt.Sprintf("%d", i), []byte{})
@@ -36,7 +38,7 @@ func TestSimpleCache_Multiple(t *testing.T) {
3638
}
3739

3840
func TestSimpleCache_MultipleLarge(t *testing.T) {
39-
simpleCache := NewSimpleCache(1000)
41+
simpleCache := NewSimpleCache(1000, 60)
4042

4143
for i := 0; i < 500000; i++ {
4244
simpleCache.Add(fmt.Sprintf("%d", i), []byte{})
@@ -50,3 +52,51 @@ func TestSimpleCache_MultipleLarge(t *testing.T) {
5052
t.Errorf("expected 999 items got %v", len(simpleCache.items))
5153
}
5254
}
55+
56+
func TestSimpleCache_AgeOut(t *testing.T) {
57+
simpleCache := &SimpleCache{
58+
maxItems: 100,
59+
items: map[string]cacheEntry{},
60+
lock: sync.Mutex{},
61+
getUnix: func() int64 {
62+
return 0
63+
},
64+
ageOutTimeSeconds: 10,
65+
}
66+
67+
for i := 0; i < 10; i++ {
68+
simpleCache.Add(fmt.Sprintf("%d", i), []byte{})
69+
}
70+
71+
// advance time
72+
simpleCache.getUnix = func() int64 {
73+
return 10000
74+
}
75+
// simulate eviction over time
76+
for i := 0; i < 10; i++ {
77+
simpleCache.ageOut()
78+
}
79+
80+
if len(simpleCache.items) != 0 {
81+
t.Errorf("expected 0 items got %v", len(simpleCache.items))
82+
}
83+
}
84+
85+
func TestSimpleCache_AgeOutTime(t *testing.T) {
86+
simpleCache := NewSimpleCache(100, 1)
87+
88+
for i := 0; i < 10; i++ {
89+
simpleCache.Add(fmt.Sprintf("%d", i), []byte{})
90+
}
91+
92+
time.Sleep(1 * time.Second)
93+
94+
// simulate eviction over time
95+
for i := 0; i < 10; i++ {
96+
simpleCache.ageOut()
97+
}
98+
99+
if len(simpleCache.items) != 0 {
100+
t.Errorf("expected 0 items got %v", len(simpleCache.items))
101+
}
102+
}

0 commit comments

Comments
 (0)