Skip to content

Commit

Permalink
Update cache for badges
Browse files Browse the repository at this point in the history
  • Loading branch information
boyter committed Sep 9, 2024
1 parent 6516a53 commit bb7e64f
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 57 deletions.
2 changes: 1 addition & 1 deletion cmd/badges/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (
)

var uniqueCode = "unique_code"
var cache = NewSimpleCache(1000)
var cache = NewSimpleCache(1000, 86400)
var countingSemaphore = make(chan bool, 1)

func main() {
Expand Down
158 changes: 105 additions & 53 deletions cmd/badges/simplecache.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,101 +9,153 @@ import (
type cacheEntry struct {
entry []byte
hits int
age int64
}

type SimpleCache struct {
maxItems int
items map[string]cacheEntry
lock sync.Mutex
maxItems int
items map[string]cacheEntry
lock sync.Mutex
getUnix func() int64
ageOutTimeSeconds int64
}

func NewSimpleCache(maxItems int) *SimpleCache {
func NewSimpleCache(maxItems int, ageOutTimeSeconds int64) *SimpleCache {
simpleCache := SimpleCache{
maxItems: maxItems,
items: map[string]cacheEntry{},
lock: sync.Mutex{},
getUnix: func() int64 {
return time.Now().Unix()
},
ageOutTimeSeconds: ageOutTimeSeconds,
}
simpleCache.runAgeItems()
return &simpleCache
}

func (cache *SimpleCache) runAgeItems() {
func (c *SimpleCache) runAgeItems() {
go func() {
for {
// maps are randomly ordered, so only decrementing 50 at a time should be acceptable
count := 50
cache.lock.Lock()
for k, v := range cache.items {
if v.hits > 0 {
v.hits--
cache.items[k] = v
}
count--
if count <= 0 {
break
}
}
cache.lock.Unlock()
time.Sleep(10 * time.Second)
}
time.Sleep(10 * time.Second)
c.adjustLfu()
c.ageOut()
}()
}

func (cache *SimpleCache) Add(cacheKey string, entry []byte) {
cache.expireItems()
func (c *SimpleCache) adjustLfu() {
c.lock.Lock()
defer c.lock.Unlock()

// maps are randomly ordered, so only decrementing 50 at a time should be acceptable
count := 50

cache.lock.Lock()
defer cache.lock.Unlock()
for k, v := range c.items {
if v.hits > 0 {
v.hits--
c.items[k] = v
}
count--
if count <= 0 {
break
}
}
}

cache.items[cacheKey] = cacheEntry{
func (c *SimpleCache) Add(cacheKey string, entry []byte) {
c.evictItems()

c.lock.Lock()
defer c.lock.Unlock()

c.items[cacheKey] = cacheEntry{
entry: entry,
hits: 1,
age: c.getUnix(),
}
}

func (cache *SimpleCache) Get(cacheKey string) ([]byte, bool) {
cache.lock.Lock()
defer cache.lock.Unlock()
func (c *SimpleCache) Get(cacheKey string) ([]byte, bool) {
c.lock.Lock()
defer c.lock.Unlock()

item, ok := cache.items[cacheKey]
item, ok := c.items[cacheKey]

if ok {
if item.hits < 100 {
item.hits++
}
cache.items[cacheKey] = item
c.items[cacheKey] = item
return item.entry, true
}

return nil, false
}

// ExpireItems is called before any insert operation because we need to ensure we have less than
// evictItems is called before any insert operation because we need to ensure we have less than
// the total number of items
func (cache *SimpleCache) expireItems() {
cache.lock.Lock()
defer cache.lock.Unlock()
func (c *SimpleCache) evictItems() {
c.lock.Lock()
defer c.lock.Unlock()

// insert process only needs to expire if we have too much
// as such if we haven't hit the limit return
if len(c.items) < c.maxItems {
return
}

count := 10
if len(cache.items) >= cache.maxItems {
lfuKey := ""
lfuLowestCount := math.MaxInt

for k, v := range cache.items {
v.hits--
cache.items[k] = v
if v.hits < lfuLowestCount {
lfuKey = k
lfuLowestCount = v.hits
}

// we only want to process X random elements so we don't spin forever
count--
if count <= 0 {
break
}
lfuKey := ""
lfuLowestCount := math.MaxInt

for k, v := range c.items {
v.hits--
c.items[k] = v
if v.hits < lfuLowestCount {
lfuKey = k
lfuLowestCount = v.hits
}

delete(cache.items, lfuKey)
// we only want to process X random elements so we don't spin forever
// however we also exit if the count is <= 0
count--
if count <= 0 || lfuLowestCount <= 0 {
break
}
}

delete(c.items, lfuKey)
}

// ageOut is called on a schedule to evict the oldest entry so long as
// its older than the configured cache time
func (c *SimpleCache) ageOut() {
// we also want to age out things eventually to avoid https://github.com/boyter/scc/discussions/435
// as such loop though and the first one that's older than a day with 0 hits is removed

c.lock.Lock()
defer c.lock.Unlock()

count := 10
lfuKey := ""
lfuOldest := int64(math.MaxInt)

// maps are un-ordered so this is acceptable
for k, v := range c.items {
c.items[k] = v
if v.age < lfuOldest {
lfuKey = k
lfuOldest = v.age
}

count--
if count <= 0 {
break
}
}

// evict the oldest but only if its older than it should be
if lfuOldest <= c.getUnix()-c.ageOutTimeSeconds {
delete(c.items, lfuKey)
}
}
56 changes: 53 additions & 3 deletions cmd/badges/simplecache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package main

import (
"fmt"
"sync"
"testing"
"time"
)

func TestSimpleCache_Add(t *testing.T) {
simpleCache := NewSimpleCache(5)
simpleCache := NewSimpleCache(5, 60)

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

func TestSimpleCache_Multiple(t *testing.T) {
simpleCache := NewSimpleCache(10)
simpleCache := NewSimpleCache(10, 60)

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

func TestSimpleCache_MultipleLarge(t *testing.T) {
simpleCache := NewSimpleCache(1000)
simpleCache := NewSimpleCache(1000, 60)

for i := 0; i < 500000; i++ {
simpleCache.Add(fmt.Sprintf("%d", i), []byte{})
Expand All @@ -50,3 +52,51 @@ func TestSimpleCache_MultipleLarge(t *testing.T) {
t.Errorf("expected 999 items got %v", len(simpleCache.items))
}
}

func TestSimpleCache_AgeOut(t *testing.T) {
simpleCache := &SimpleCache{
maxItems: 100,
items: map[string]cacheEntry{},
lock: sync.Mutex{},
getUnix: func() int64 {
return 0
},
ageOutTimeSeconds: 10,
}

for i := 0; i < 10; i++ {
simpleCache.Add(fmt.Sprintf("%d", i), []byte{})
}

// advance time
simpleCache.getUnix = func() int64 {
return 10000
}
// simulate eviction over time
for i := 0; i < 10; i++ {
simpleCache.ageOut()
}

if len(simpleCache.items) != 0 {
t.Errorf("expected 0 items got %v", len(simpleCache.items))
}
}

func TestSimpleCache_AgeOutTime(t *testing.T) {
simpleCache := NewSimpleCache(100, 1)

for i := 0; i < 10; i++ {
simpleCache.Add(fmt.Sprintf("%d", i), []byte{})
}

time.Sleep(1 * time.Second)

// simulate eviction over time
for i := 0; i < 10; i++ {
simpleCache.ageOut()
}

if len(simpleCache.items) != 0 {
t.Errorf("expected 0 items got %v", len(simpleCache.items))
}
}

0 comments on commit bb7e64f

Please sign in to comment.