Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 49 additions & 28 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
30 changes: 30 additions & 0 deletions cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
73 changes: 47 additions & 26 deletions v2/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
27 changes: 27 additions & 0 deletions v2/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
25 changes: 23 additions & 2 deletions v3/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}

Expand All @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down
26 changes: 26 additions & 0 deletions v3/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down