From c9c79def63e054e5cde291d1a692ada197635308 Mon Sep 17 00:00:00 2001 From: Tehhs Date: Sat, 11 Oct 2025 03:26:09 +1100 Subject: [PATCH 1/6] Add ContainsOrAdd(key, value) to v3 cache --- v3/cache.go | 18 ++++++++++++++++++ v3/cache_test.go | 21 +++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/v3/cache.go b/v3/cache.go index 4b6d9b4..2421045 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 @@ -151,6 +152,23 @@ 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() + + _, containsKey := c.items[key] + if containsKey { + c.Unlock() + return true, false + } + + c.Unlock() //addWithTTL() Locks + 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..68908c5 100644 --- a/v3/cache_test.go +++ b/v3/cache_test.go @@ -394,6 +394,27 @@ 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) + + _, 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) From 37b64fdcdb89cd0813f51aee360cd19798741caa Mon Sep 17 00:00:00 2001 From: Tehhs Date: Sat, 11 Oct 2025 11:07:27 +1100 Subject: [PATCH 2/6] Add ContainsOrAdd(key, value, ttl) to v2 cache --- v2/cache.go | 71 ++++++++++++++++++++++++++++++------------------ v2/cache_test.go | 19 +++++++++++++ 2 files changed, 64 insertions(+), 26 deletions(-) diff --git a/v2/cache.go b/v2/cache.go index 50b194f..85fe7a4 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] + ContainsOrAdd(key K, value V) bool Set(key K, value V, ttl time.Duration) Get(key K) (V, bool) Peek(key K) (V, bool) @@ -73,39 +74,23 @@ 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 with the +// default TTL. +func (c *cacheImpl[K, V]) ContainsOrAdd(key K, value V) 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) - } + if _, ok := c.items[key]; ok { + return true } - // Verify size not exceeded - if c.maxKeys > 0 && len(c.items) > c.maxKeys { - c.removeOldest() - } + c.set(key, value, c.ttl) + return false } // Get returns the key value if it's not expired @@ -247,6 +232,40 @@ func (c *cacheImpl[K, V]) removeOldest() { } } +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..771bfa9 100644 --- a/v2/cache_test.go +++ b/v2/cache_test.go @@ -293,6 +293,25 @@ 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()) + + lc.Set("key2", "val2", 0) + assert.Equal(t, 2, lc.Len()) + + contains := lc.ContainsOrAdd("key1", "value") + assert.Equal(t, true, contains) + + contains = lc.ContainsOrAdd("key3", "val3") + assert.Equal(t, false, contains) + + _, 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) From 5b88c0dceb6216081e2745b5957de674d24e0ab6 Mon Sep 17 00:00:00 2001 From: Tehhs Date: Sat, 11 Oct 2025 11:15:56 +1100 Subject: [PATCH 3/6] tidy --- v2/cache.go | 10 +++++----- v3/cache.go | 3 +-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/v2/cache.go b/v2/cache.go index 85fe7a4..1f374e4 100644 --- a/v2/cache.go +++ b/v2/cache.go @@ -83,14 +83,14 @@ func (c *cacheImpl[K, V]) Set(key K, value V, ttl time.Duration) { // Returns true if cache already contains the key. If the cache does // not contain the key, the key will be set with the value with the // default TTL. -func (c *cacheImpl[K, V]) ContainsOrAdd(key K, value V) bool { +func (c *cacheImpl[K, V]) ContainsOrAdd(key K, value V) bool { - if _, ok := c.items[key]; ok { - return true + if _, ok := c.items[key]; ok { + return true } c.set(key, value, c.ttl) - return false + return false } // Get returns the key value if it's not expired @@ -232,6 +232,7 @@ 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 @@ -266,7 +267,6 @@ func (c *cacheImpl[K, V]) set(key K, value V, ttl time.Duration) { } } - // 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) { c.evictList.Remove(e) diff --git a/v3/cache.go b/v3/cache.go index 2421045..2eaa9e5 100644 --- a/v3/cache.go +++ b/v3/cache.go @@ -158,8 +158,7 @@ func (c *cacheImpl[K, V]) Get(key K) (V, bool) { func (c *cacheImpl[K, V]) ContainsOrAdd(key K, value V) (bool, bool) { c.Lock() - _, containsKey := c.items[key] - if containsKey { + if _, containsKey := c.items[key]; containsKey { c.Unlock() return true, false } From 693edc66cd6858adb2ac2e5bf8077da7eaae237c Mon Sep 17 00:00:00 2001 From: Tehhs Date: Sat, 11 Oct 2025 11:45:07 +1100 Subject: [PATCH 4/6] v1 support + changes v2 and v1 dont have "ContainsOrAdd" but instead have "ContainsOrSet", as v2 and v1 don't have "Add" functions but v3 does. --- cache.go | 76 ++++++++++++++++++++++++++++++------------------ cache_test.go | 20 +++++++++++++ v2/cache.go | 12 ++++---- v2/cache_test.go | 4 +-- 4 files changed, 77 insertions(+), 35 deletions(-) diff --git a/cache.go b/cache.go index 0053b7d..9fe6e28 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,39 @@ func (c *cacheImpl) removeOldest() { } } +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..a89964c 100644 --- a/cache_test.go +++ b/cache_test.go @@ -309,6 +309,26 @@ 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()) + + lc.Set("key2", "val2", 0) + assert.Equal(t, 2, lc.Len()) + + contains := lc.ContainsOrSet("key1", "value", 0) + assert.Equal(t, true, contains) + + contains = lc.ContainsOrSet("key3", "val3", 0) + assert.Equal(t, false, contains) + + _, ok := lc.Get("key1") + assert.Equal(t, false, ok) +} + 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 1f374e4..c1fad30 100644 --- a/v2/cache.go +++ b/v2/cache.go @@ -23,7 +23,7 @@ import ( type Cache[K comparable, V any] interface { fmt.Stringer options[K, V] - ContainsOrAdd(key K, value V) bool + 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) @@ -81,15 +81,17 @@ func (c *cacheImpl[K, V]) Set(key K, value V, ttl time.Duration) { } // Returns true if cache already contains the key. If the cache does -// not contain the key, the key will be set with the value with the -// default TTL. -func (c *cacheImpl[K, V]) ContainsOrAdd(key K, value V) bool { +// 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 { + + c.Lock() + defer c.Unlock() if _, ok := c.items[key]; ok { return true } - c.set(key, value, c.ttl) + c.set(key, value, ttl) return false } diff --git a/v2/cache_test.go b/v2/cache_test.go index 771bfa9..fdaa6c0 100644 --- a/v2/cache_test.go +++ b/v2/cache_test.go @@ -302,10 +302,10 @@ func TestCacheContainsOrAdd(t *testing.T) { lc.Set("key2", "val2", 0) assert.Equal(t, 2, lc.Len()) - contains := lc.ContainsOrAdd("key1", "value") + contains := lc.ContainsOrSet("key1", "value", 0) assert.Equal(t, true, contains) - contains = lc.ContainsOrAdd("key3", "val3") + contains = lc.ContainsOrSet("key3", "val3", 0) assert.Equal(t, false, contains) _, ok := lc.Get("key1") From f30e34104b6230a11dad4c652b8250ca23f1d4c4 Mon Sep 17 00:00:00 2001 From: Tehhs Date: Sat, 11 Oct 2025 11:50:22 +1100 Subject: [PATCH 5/6] moved locking out of addWithTTL and into calling functions --- v3/cache.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/v3/cache.go b/v3/cache.go index 2eaa9e5..58fcc2d 100644 --- a/v3/cache.go +++ b/v3/cache.go @@ -83,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) } @@ -99,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) @@ -157,13 +162,12 @@ func (c *cacheImpl[K, V]) Get(key K) (V, bool) { // 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 { - c.Unlock() return true, false } - c.Unlock() //addWithTTL() Locks evicted := c.addWithTTL(key, value, c.ttl) return false, evicted } From 9170407b66f92a5a9b19667ff5a76d63beabb64e Mon Sep 17 00:00:00 2001 From: Tehhs Date: Sat, 11 Oct 2025 12:12:13 +1100 Subject: [PATCH 6/6] improved ContainsOrAdd() tests --- cache.go | 1 + cache_test.go | 18 ++++++++++++++---- v2/cache_test.go | 16 ++++++++++++---- v3/cache_test.go | 7 ++++++- 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/cache.go b/cache.go index 9fe6e28..a49d4fb 100644 --- a/cache.go +++ b/cache.go @@ -238,6 +238,7 @@ 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 diff --git a/cache_test.go b/cache_test.go index a89964c..1d456ee 100644 --- a/cache_test.go +++ b/cache_test.go @@ -316,17 +316,27 @@ func TestCacheContainsOrAdd(t *testing.T) { lc.Set("key1", "val1", 0) assert.Equal(t, 1, lc.Len()) - lc.Set("key2", "val2", 0) + // 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) - contains := lc.ContainsOrSet("key1", "value", 0) + // 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) - _, ok := lc.Get("key1") - assert.Equal(t, false, ok) + // 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() { diff --git a/v2/cache_test.go b/v2/cache_test.go index fdaa6c0..92e0c73 100644 --- a/v2/cache_test.go +++ b/v2/cache_test.go @@ -299,17 +299,25 @@ func TestCacheContainsOrAdd(t *testing.T) { lc.Set("key1", "val1", 0) assert.Equal(t, 1, lc.Len()) - lc.Set("key2", "val2", 0) + // 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) - contains := lc.ContainsOrSet("key1", "value", 0) + // 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) - _, ok := lc.Get("key1") - assert.Equal(t, false, ok) + // Make sure function is setting value properly + val, ok := lc.Get("key2") + assert.Equal(t, true, ok) + assert.Equal(t, "val2", val) } func ExampleCache() { diff --git a/v3/cache_test.go b/v3/cache_test.go index 68908c5..ee579cb 100644 --- a/v3/cache_test.go +++ b/v3/cache_test.go @@ -411,7 +411,12 @@ func TestCacheContainsOrAdd(t *testing.T) { assert.Equal(t, false, contains) assert.Equal(t, true, evicted) - _, ok := lc.Get("key1") + 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) }