From 42f9f7fb5dec74f8e5a95b19c1efd0c8b846e486 Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Fri, 1 May 2020 12:19:19 +0100 Subject: [PATCH] lib/cache: add Rename, Pin and Unpin --- lib/cache/cache.go | 45 ++++++++++++++++++++- lib/cache/cache_test.go | 89 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 132 insertions(+), 2 deletions(-) diff --git a/lib/cache/cache.go b/lib/cache/cache.go index 59d853af4..7fa66392f 100644 --- a/lib/cache/cache.go +++ b/lib/cache/cache.go @@ -33,6 +33,7 @@ type cacheEntry struct { err error // creation error key string // key lastUsed time.Time // time used for expiry + pinCount int // non zero if the entry should not be removed } // CreateFunc is called to create new values. If the create function @@ -74,6 +75,26 @@ func (c *Cache) Get(key string, create CreateFunc) (value interface{}, err error return entry.value, entry.err } +func (c *Cache) addPin(key string, count int) { + c.mu.Lock() + entry, ok := c.cache[key] + if ok { + entry.pinCount += count + c.used(entry) + } + c.mu.Unlock() +} + +// Pin a value in the cache if it exists +func (c *Cache) Pin(key string) { + c.addPin(key, 1) +} + +// Unpin a value in the cache if it exists +func (c *Cache) Unpin(key string) { + c.addPin(key, -1) +} + // Put puts an value named key into the cache func (c *Cache) Put(key string, value interface{}) { c.mu.Lock() @@ -98,13 +119,35 @@ func (c *Cache) GetMaybe(key string) (value interface{}, found bool) { return entry.value, found } +// Rename renames the item at oldKey to newKey. +// +// If there was an existing item at newKey then it takes precedence +// and is returned otherwise the item (if any) at oldKey is returned. +func (c *Cache) Rename(oldKey, newKey string) (value interface{}, found bool) { + c.mu.Lock() + if newEntry, newFound := c.cache[newKey]; newFound { + // If new entry is found use that + delete(c.cache, oldKey) + value, found = newEntry.value, newFound + c.used(newEntry) + } else if oldEntry, oldFound := c.cache[oldKey]; oldFound { + // If old entry is found rename it to new and use that + c.cache[newKey] = oldEntry + delete(c.cache, oldKey) + c.used(oldEntry) + value, found = oldEntry.value, oldFound + } + c.mu.Unlock() + return value, found +} + // cacheExpire expires any entries that haven't been used recently func (c *Cache) cacheExpire() { c.mu.Lock() defer c.mu.Unlock() now := time.Now() for key, entry := range c.cache { - if now.Sub(entry.lastUsed) > c.expireDuration { + if entry.pinCount <= 0 && now.Sub(entry.lastUsed) > c.expireDuration { delete(c.cache, key) } } diff --git a/lib/cache/cache_test.go b/lib/cache/cache_test.go index cfdc28023..d2811e66d 100644 --- a/lib/cache/cache_test.go +++ b/lib/cache/cache_test.go @@ -106,22 +106,74 @@ func TestCacheExpire(t *testing.T) { c.mu.Lock() entry := c.cache["/"] - assert.Equal(t, 1, len(c.cache)) c.mu.Unlock() + c.cacheExpire() + c.mu.Lock() assert.Equal(t, 1, len(c.cache)) entry.lastUsed = time.Now().Add(-c.expireDuration - 60*time.Second) assert.Equal(t, true, c.expireRunning) c.mu.Unlock() + time.Sleep(250 * time.Millisecond) + c.mu.Lock() assert.Equal(t, false, c.expireRunning) assert.Equal(t, 0, len(c.cache)) c.mu.Unlock() } +func TestCachePin(t *testing.T) { + c, create := setup(t) + + _, err := c.Get("/", create) + require.NoError(t, err) + + // Pin a non existent item to show nothing happens + c.Pin("notfound") + + c.mu.Lock() + entry := c.cache["/"] + assert.Equal(t, 1, len(c.cache)) + c.mu.Unlock() + + c.cacheExpire() + + c.mu.Lock() + assert.Equal(t, 1, len(c.cache)) + c.mu.Unlock() + + // Pin the entry and check it does not get expired + c.Pin("/") + + // Reset last used to make the item expirable + c.mu.Lock() + entry.lastUsed = time.Now().Add(-c.expireDuration - 60*time.Second) + c.mu.Unlock() + + c.cacheExpire() + + c.mu.Lock() + assert.Equal(t, 1, len(c.cache)) + c.mu.Unlock() + + // Unpin the entry and check it does get expired now + c.Unpin("/") + + // Reset last used + c.mu.Lock() + entry.lastUsed = time.Now().Add(-c.expireDuration - 60*time.Second) + c.mu.Unlock() + + c.cacheExpire() + + c.mu.Lock() + assert.Equal(t, 0, len(c.cache)) + c.mu.Unlock() +} + func TestClear(t *testing.T) { c, create := setup(t) @@ -172,3 +224,38 @@ func TestGetMaybe(t *testing.T) { assert.Equal(t, false, found) assert.Nil(t, value) } + +func TestCacheRename(t *testing.T) { + c := New() + create := func(path string) (interface{}, bool, error) { + return path, true, nil + } + + existing1, err := c.Get("existing1", create) + require.NoError(t, err) + _, err = c.Get("existing2", create) + require.NoError(t, err) + + assert.Equal(t, 2, c.Entries()) + + // rename to non existent + value, found := c.Rename("existing1", "EXISTING1") + assert.Equal(t, true, found) + assert.Equal(t, existing1, value) + + assert.Equal(t, 2, c.Entries()) + + // rename to existent and check existing value is returned + value, found = c.Rename("existing2", "EXISTING1") + assert.Equal(t, true, found) + assert.Equal(t, existing1, value) + + assert.Equal(t, 1, c.Entries()) + + // rename non existent + value, found = c.Rename("notfound", "NOTFOUND") + assert.Equal(t, false, found) + assert.Nil(t, value) + + assert.Equal(t, 1, c.Entries()) +}