diff --git a/cache.go b/cache.go index 0053b7d..a49d4fb 100644 --- a/cache.go +++ b/cache.go @@ -22,6 +22,7 @@ import ( // Cache defines cache interface type Cache interface { fmt.Stringer + ContainsOrSet(key string, value interface{}, ttl time.Duration) bool Set(key string, value interface{}, ttl time.Duration) Get(key string) (interface{}, bool) Peek(key string) (interface{}, bool) @@ -77,41 +78,27 @@ func NewCache(options ...Option) (Cache, error) { return &res, nil } -// Set key, ttl of 0 would use cache-wide TTL -func (c *cacheImpl) Set(key string, value interface{}, ttl time.Duration) { - if ttl == 0 { - ttl = c.ttl - } - now := time.Now() +// Returns true if cache already contains the key. If the cache does +// not contain the key, the key will be set with the value and return false +func (c *cacheImpl) ContainsOrSet(key string, value interface{}, ttl time.Duration) bool { + c.Lock() defer c.Unlock() - // Check for existing item - if ent, ok := c.items[key]; ok { - c.evictList.MoveToFront(ent) - ent.Value.(*cacheItem).value = value - ent.Value.(*cacheItem).expiresAt = now.Add(ttl) - return + if _, ok := c.items[key]; ok { + return true } - // Add new item - ent := &cacheItem{key: key, value: value, expiresAt: now.Add(ttl)} - entry := c.evictList.PushFront(ent) - c.items[key] = entry - c.stat.Added++ + c.set(key, value, ttl) + return false +} - // Remove oldest entry if it is expired, only in case of non-default TTL. - if c.ttl != noEvictionTTL || ttl != noEvictionTTL { - ent := c.evictList.Back() - if ent != nil && now.After(ent.Value.(*cacheItem).expiresAt) { - c.removeElement(ent) - } - } +// Set key, ttl of 0 would use cache-wide TTL +func (c *cacheImpl) Set(key string, value interface{}, ttl time.Duration) { + c.Lock() + defer c.Unlock() - // Verify size not exceeded - if c.maxKeys > 0 && len(c.items) > c.maxKeys { - c.removeOldest() - } + c.set(key, value, ttl) } // Get returns the key value if it's not expired @@ -251,6 +238,40 @@ func (c *cacheImpl) removeOldest() { } } +// Set key, ttl of 0 would use cache-wide TTL +func (c *cacheImpl) set(key string, value interface{}, ttl time.Duration) { + if ttl == 0 { + ttl = c.ttl + } + now := time.Now() + + // Check for existing item + if ent, ok := c.items[key]; ok { + c.evictList.MoveToFront(ent) + ent.Value.(*cacheItem).value = value + ent.Value.(*cacheItem).expiresAt = now.Add(ttl) + return + } + + // Add new item + ent := &cacheItem{key: key, value: value, expiresAt: now.Add(ttl)} + entry := c.evictList.PushFront(ent) + c.items[key] = entry + c.stat.Added++ + + // Remove oldest entry if it is expired, only in case of non-default TTL. + if c.ttl != noEvictionTTL || ttl != noEvictionTTL { + ent := c.evictList.Back() + if ent != nil && now.After(ent.Value.(*cacheItem).expiresAt) { + c.removeElement(ent) + } + } + + // Verify size not exceeded + if c.maxKeys > 0 && len(c.items) > c.maxKeys { + c.removeOldest() + } +} // removeElement is used to remove a given list element from the cache. Has to be called with lock! func (c *cacheImpl) removeElement(e *list.Element) { diff --git a/cache_test.go b/cache_test.go index 09bf120..1d456ee 100644 --- a/cache_test.go +++ b/cache_test.go @@ -309,6 +309,36 @@ func TestCacheRemoveOldest(t *testing.T) { assert.Equal(t, 1, lc.Len()) } +func TestCacheContainsOrAdd(t *testing.T) { + lc, err := NewCache(LRU(), MaxKeys(2)) + assert.NoError(t, err) + + lc.Set("key1", "val1", 0) + assert.Equal(t, 1, lc.Len()) + + // Make sure function sets key and + // adds to cache + contains := lc.ContainsOrSet("key2", "val2", 0) + assert.Equal(t, 2, lc.Len()) + assert.Equal(t, false, contains) + + // Make sure function returns true if contains key + // and doesn't add to cache + contains = lc.ContainsOrSet("key1", "value", 0) + assert.Equal(t, true, contains) + assert.Equal(t, 2, lc.Len()) + + contains = lc.ContainsOrSet("key3", "val3", 0) + assert.Equal(t, false, contains) + + // Make sure function is setting value properly + r, ok := lc.Get("key2") + assert.Equal(t, true, ok) + val := r.(string) + assert.Equal(t, "val2", val) + +} + func ExampleCache() { // make cache with short TTL and 3 max keys cache, _ := NewCache(MaxKeys(3), TTL(time.Millisecond*10)) diff --git a/v2/cache.go b/v2/cache.go index 50b194f..c1fad30 100644 --- a/v2/cache.go +++ b/v2/cache.go @@ -23,6 +23,7 @@ import ( type Cache[K comparable, V any] interface { fmt.Stringer options[K, V] + ContainsOrSet(key K, value V, ttl time.Duration) bool Set(key K, value V, ttl time.Duration) Get(key K) (V, bool) Peek(key K) (V, bool) @@ -73,39 +74,25 @@ func NewCache[K comparable, V any]() Cache[K, V] { // Set key, ttl of 0 would use cache-wide TTL func (c *cacheImpl[K, V]) Set(key K, value V, ttl time.Duration) { - if ttl == 0 { - ttl = c.ttl - } - now := time.Now() c.Lock() defer c.Unlock() - // Check for existing item - if ent, ok := c.items[key]; ok { - c.evictList.MoveToFront(ent) - ent.Value.(*cacheItem[K, V]).value = value - ent.Value.(*cacheItem[K, V]).expiresAt = now.Add(ttl) - return - } + c.set(key, value, ttl) +} - // Add new item - ent := &cacheItem[K, V]{key: key, value: value, expiresAt: now.Add(ttl)} - entry := c.evictList.PushFront(ent) - c.items[key] = entry - c.stat.Added++ +// Returns true if cache already contains the key. If the cache does +// not contain the key, the key will be set with the value and return false +func (c *cacheImpl[K, V]) ContainsOrSet(key K, value V, ttl time.Duration) bool { - // Remove oldest entry if it is expired, only in case of non-default TTL. - if c.ttl != noEvictionTTL || ttl != noEvictionTTL { - ent := c.evictList.Back() - if ent != nil && now.After(ent.Value.(*cacheItem[K, V]).expiresAt) { - c.removeElement(ent) - } - } + c.Lock() + defer c.Unlock() - // Verify size not exceeded - if c.maxKeys > 0 && len(c.items) > c.maxKeys { - c.removeOldest() + if _, ok := c.items[key]; ok { + return true } + + c.set(key, value, ttl) + return false } // Get returns the key value if it's not expired @@ -247,6 +234,40 @@ func (c *cacheImpl[K, V]) removeOldest() { } } +// Set key, ttl of 0 would use cache-wide TTL +func (c *cacheImpl[K, V]) set(key K, value V, ttl time.Duration) { + if ttl == 0 { + ttl = c.ttl + } + now := time.Now() + + // Check for existing item + if ent, ok := c.items[key]; ok { + c.evictList.MoveToFront(ent) + ent.Value.(*cacheItem[K, V]).value = value + ent.Value.(*cacheItem[K, V]).expiresAt = now.Add(ttl) + return + } + + // Add new item + ent := &cacheItem[K, V]{key: key, value: value, expiresAt: now.Add(ttl)} + entry := c.evictList.PushFront(ent) + c.items[key] = entry + c.stat.Added++ + + // Remove oldest entry if it is expired, only in case of non-default TTL. + if c.ttl != noEvictionTTL || ttl != noEvictionTTL { + ent := c.evictList.Back() + if ent != nil && now.After(ent.Value.(*cacheItem[K, V]).expiresAt) { + c.removeElement(ent) + } + } + + // Verify size not exceeded + if c.maxKeys > 0 && len(c.items) > c.maxKeys { + c.removeOldest() + } +} // removeElement is used to remove a given list element from the cache. Has to be called with lock! func (c *cacheImpl[K, V]) removeElement(e *list.Element) { diff --git a/v2/cache_test.go b/v2/cache_test.go index cdbbf6f..92e0c73 100644 --- a/v2/cache_test.go +++ b/v2/cache_test.go @@ -293,6 +293,33 @@ func TestCacheRemoveOldest(t *testing.T) { assert.Equal(t, 1, lc.Len()) } +func TestCacheContainsOrAdd(t *testing.T) { + lc := NewCache[string, string]().WithLRU().WithMaxKeys(2) + + lc.Set("key1", "val1", 0) + assert.Equal(t, 1, lc.Len()) + + // Make sure function sets key and + // adds to cache + contains := lc.ContainsOrSet("key2", "val2", 0) + assert.Equal(t, 2, lc.Len()) + assert.Equal(t, false, contains) + + // Make sure function returns true if contains key + // and doesn't add to cache + contains = lc.ContainsOrSet("key1", "value", 0) + assert.Equal(t, true, contains) + assert.Equal(t, 2, lc.Len()) + + contains = lc.ContainsOrSet("key3", "val3", 0) + assert.Equal(t, false, contains) + + // Make sure function is setting value properly + val, ok := lc.Get("key2") + assert.Equal(t, true, ok) + assert.Equal(t, "val2", val) +} + func ExampleCache() { // make cache with short TTL and 3 max keys cache := NewCache[string, string]().WithMaxKeys(3).WithTTL(time.Millisecond * 10) diff --git a/v3/cache.go b/v3/cache.go index 4b6d9b4..58fcc2d 100644 --- a/v3/cache.go +++ b/v3/cache.go @@ -28,6 +28,7 @@ type Cache[K comparable, V any] interface { Get(key K) (V, bool) GetExpiration(key K) (time.Time, bool) GetOldest() (K, V, bool) + ContainsOrAdd(key K, value V) (bool, bool) Contains(key K) (ok bool) Peek(key K) (V, bool) Values() []V @@ -82,11 +83,17 @@ func NewCache[K comparable, V any]() Cache[K, V] { // Returns false if there was no eviction: the item was already in the cache, // or the size was not exceeded. func (c *cacheImpl[K, V]) Add(key K, value V) (evicted bool) { + c.Lock() + defer c.Unlock() + return c.addWithTTL(key, value, c.ttl) } // Set key, ttl of 0 would use cache-wide TTL func (c *cacheImpl[K, V]) Set(key K, value V, ttl time.Duration) { + c.Lock() + defer c.Unlock() + c.addWithTTL(key, value, ttl) } @@ -98,8 +105,7 @@ func (c *cacheImpl[K, V]) addWithTTL(key K, value V, ttl time.Duration) (evicted ttl = c.ttl } now := time.Now() - c.Lock() - defer c.Unlock() + // Check for existing item if ent, ok := c.items[key]; ok { c.evictList.MoveToFront(ent) @@ -151,6 +157,21 @@ func (c *cacheImpl[K, V]) Get(key K) (V, bool) { return def, false } +// ContainsOrAdd checks if a key is in the cache without updating the +// recent-ness or deleting it for being stale, and if not, adds the value. +// Returns whether found and whether an eviction occurred. +func (c *cacheImpl[K, V]) ContainsOrAdd(key K, value V) (bool, bool) { + c.Lock() + defer c.Unlock() + + if _, containsKey := c.items[key]; containsKey { + return true, false + } + + evicted := c.addWithTTL(key, value, c.ttl) + return false, evicted +} + // Contains checks if a key is in the cache, without updating the recent-ness // or deleting it for being stale. func (c *cacheImpl[K, V]) Contains(key K) (ok bool) { diff --git a/v3/cache_test.go b/v3/cache_test.go index ea56d48..ee579cb 100644 --- a/v3/cache_test.go +++ b/v3/cache_test.go @@ -394,6 +394,32 @@ func TestCacheRemoveOldest(t *testing.T) { } +func TestCacheContainsOrAdd(t *testing.T) { + lc := NewCache[string, string]().WithLRU().WithMaxKeys(2) + + lc.Add("key1", "val1") + assert.Equal(t, 1, lc.Len()) + + lc.Add("key2", "val2") + assert.Equal(t, 2, lc.Len()) + + contains, evicted := lc.ContainsOrAdd("key1", "val1") + assert.Equal(t, true, contains) + assert.Equal(t, false, evicted) + + contains, evicted = lc.ContainsOrAdd("key3", "val3") + assert.Equal(t, false, contains) + assert.Equal(t, true, evicted) + + val, ok := lc.Get("key3") + assert.Equal(t, true, ok) + assert.Equal(t, "val3", val) + + // Make sure key1 evicted + _, ok = lc.Get("key1") + assert.Equal(t, false, ok) +} + func ExampleCache() { // make cache with short TTL and 3 max keys cache := NewCache[string, string]().WithMaxKeys(3).WithTTL(time.Millisecond * 10)