diff --git a/cmd/badges/main.go b/cmd/badges/main.go index 35af8b092..d2c125f66 100644 --- a/cmd/badges/main.go +++ b/cmd/badges/main.go @@ -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() { diff --git a/cmd/badges/simplecache.go b/cmd/badges/simplecache.go index fb4b74f98..abb72600a 100644 --- a/cmd/badges/simplecache.go +++ b/cmd/badges/simplecache.go @@ -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) } } diff --git a/cmd/badges/simplecache_test.go b/cmd/badges/simplecache_test.go index f9862cada..11654d283 100644 --- a/cmd/badges/simplecache_test.go +++ b/cmd/badges/simplecache_test.go @@ -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{}) @@ -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{}) @@ -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{}) @@ -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)) + } +}