Skip to content

Commit

Permalink
add TTL to Set() to support custom settings per-entry
Browse files Browse the repository at this point in the history
  • Loading branch information
paskal committed May 12, 2020
1 parent 0e5fa01 commit b0beef9
Show file tree
Hide file tree
Showing 3 changed files with 37 additions and 33 deletions.
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ func main() {
// make cache with short TTL and 3 max keys
lc, _ := cache.NewLoadingCache(cache.MaxKeys(3), cache.TTL(time.Millisecond*10))

// set value under key1
lc.Set("key1", "val1")
// set value under key1.
// with 0 ttl (last parameter) will use cache-wide setting instead (10ms).
lc.Set("key1", "val1", 0)

// get value under key1
r, ok := lc.Get("key1")
Expand All @@ -51,11 +52,12 @@ func main() {

// get value under key1 after key expiration
r, ok = lc.Get("key1")
// don't convert to string as with ok == false value vould be nil
// don't convert to string as with ok == false value would be nil
fmt.Printf("value after expiration is found: %v, value: %v\n", ok, r)

// set value under key2, would evict old entry because it is already expired
lc.Set("key2", "val2")
// set value under key2, would evict old entry because it is already expired.
// ttl (last parameter) overrides cache-wide ttl.
lc.Set("key2", "val2", time.Minute*5)

fmt.Printf("%+v\n", lc)
// Output:
Expand Down
26 changes: 13 additions & 13 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (
// LoadingCache defines loading cache interface
type LoadingCache interface {
fmt.Stringer
Set(key string, value interface{})
Set(key string, value interface{}, ttl time.Duration)
Get(key string) (interface{}, bool)
Peek(key string) (interface{}, bool)
Keys() []string
Expand Down Expand Up @@ -79,35 +79,37 @@ func NewLoadingCache(options ...Option) (LoadingCache, error) {
return &res, nil
}

// Set key
func (c *loadingCacheImpl) Set(key string, value interface{}) {
// Set key, ttl of 0 would use cache-wide TTL
func (c *loadingCacheImpl) Set(key string, value interface{}, ttl time.Duration) {
c.Lock()
defer c.Unlock()
now := time.Now()
if ttl == 0 {
ttl = c.ttl
}

// 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(c.ttl)
ent.Value.(*cacheItem).expiresAt = now.Add(ttl)
return
}

// Add new item
ent := &cacheItem{key: key, value: value, expiresAt: now.Add(c.ttl)}
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 {
if c.ttl != noEvictionTTL || ttl != noEvictionTTL {
c.removeOldestIfExpired()
}

// Verify size not exceeded
if c.maxKeys > 0 && len(c.items) > c.maxKeys {
c.removeOldest()
return
}
}

Expand All @@ -132,16 +134,20 @@ func (c *loadingCacheImpl) Get(key string) (interface{}, bool) {
}

// Peek returns the key value (or undefined if not found) without updating the "recently used"-ness of the key.
// Works exactly the same as Get in case of LRC mode (default one).
func (c *loadingCacheImpl) Peek(key string) (interface{}, bool) {
c.Lock()
defer c.Unlock()
if ent, ok := c.items[key]; ok {
// Expired item check
if time.Now().After(ent.Value.(*cacheItem).expiresAt) {
c.stat.Misses++
return nil, false
}
c.stat.Hits++
return ent.Value.(*cacheItem).value, true
}
c.stat.Misses++
return nil, false
}

Expand Down Expand Up @@ -193,12 +199,6 @@ func (c *loadingCacheImpl) DeleteExpired() {
for _, key := range c.keys() {
if time.Now().After(c.items[key].Value.(*cacheItem).expiresAt) {
c.removeElement(c.items[key])
continue
}
// if cache is not LRU, keys() are sorted by expiresAt and there are no
// more expired entries left at this point
if !c.isLRU {
return
}
}
}
Expand Down
32 changes: 17 additions & 15 deletions cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func TestLoadingCacheNoPurge(t *testing.T) {
lc, err := NewLoadingCache()
assert.NoError(t, err)

lc.Set("key1", "val1")
lc.Set("key1", "val1", 0)
assert.Equal(t, 1, lc.Len())

v, ok := lc.Peek("key1")
Expand All @@ -36,7 +36,7 @@ func TestLoadingCacheWithDeleteExpired(t *testing.T) {
)
assert.NoError(t, err)

lc.Set("key1", "val1")
lc.Set("key1", "val1", 0)

time.Sleep(100 * time.Millisecond) // not enough to expire
lc.DeleteExpired()
Expand All @@ -56,7 +56,7 @@ func TestLoadingCacheWithDeleteExpired(t *testing.T) {
assert.Equal(t, []string{"key1", "val1"}, evicted)

// add new entry
lc.Set("key2", "val2")
lc.Set("key2", "val2", 0)
assert.Equal(t, 1, lc.Len())

// nothing deleted
Expand All @@ -76,7 +76,7 @@ func TestLoadingCacheWithPurgeEnforcedBySize(t *testing.T) {

for i := 0; i < 100; i++ {
i := i
lc.Set(fmt.Sprintf("key%d", i), fmt.Sprintf("val%d", i))
lc.Set(fmt.Sprintf("key%d", i), fmt.Sprintf("val%d", i), 0)
v, ok := lc.Get(fmt.Sprintf("key%d", i))
assert.Equal(t, fmt.Sprintf("val%d", i), v)
assert.True(t, ok)
Expand All @@ -93,7 +93,7 @@ func TestLoadingCacheConcurrency(t *testing.T) {
wg.Add(1000)
for i := 0; i < 1000; i++ {
go func(i int) {
lc.Set(fmt.Sprintf("key-%d", i/10), fmt.Sprintf("val-%d", i/10))
lc.Set(fmt.Sprintf("key-%d", i/10), fmt.Sprintf("val-%d", i/10), 0)
wg.Done()
}(i)
}
Expand All @@ -106,8 +106,8 @@ func TestLoadingCacheInvalidateAndEvict(t *testing.T) {
lc, err := NewLoadingCache(LRU(), OnEvicted(func(_ string, _ interface{}) { evicted++ }))
assert.NoError(t, err)

lc.Set("key1", "val1")
lc.Set("key2", "val2")
lc.Set("key1", "val1", 0)
lc.Set("key2", "val2", 0)

val, ok := lc.Get("key1")
assert.True(t, ok)
Expand Down Expand Up @@ -145,7 +145,7 @@ func TestLoadingExpired(t *testing.T) {
lc, err := NewLoadingCache(TTL(time.Millisecond * 5))
assert.NoError(t, err)

lc.Set("key1", "val1")
lc.Set("key1", "val1", 0)
assert.Equal(t, 1, lc.Len())

v, ok := lc.Peek("key1")
Expand All @@ -172,7 +172,7 @@ func TestLoadingCacheRemoveOldest(t *testing.T) {
lc, err := NewLoadingCache(LRU(), MaxKeys(2))
assert.NoError(t, err)

lc.Set("key1", "val1")
lc.Set("key1", "val1", 0)
assert.Equal(t, 1, lc.Len())

v, ok := lc.Get("key1")
Expand All @@ -182,7 +182,7 @@ func TestLoadingCacheRemoveOldest(t *testing.T) {
assert.Equal(t, []string{"key1"}, lc.Keys())
assert.Equal(t, 1, lc.Len())

lc.Set("key2", "val2")
lc.Set("key2", "val2", 0)
assert.Equal(t, []string{"key1", "key2"}, lc.Keys())
assert.Equal(t, 2, lc.Len())

Expand All @@ -196,8 +196,9 @@ func ExampleLoadingCache() {
// make cache with short TTL and 3 max keys
cache, _ := NewLoadingCache(MaxKeys(3), TTL(time.Millisecond*10))

// set value under key1
cache.Set("key1", "val1")
// set value under key1.
// with 0 ttl (last parameter) will use cache-wide setting instead (10ms).
cache.Set("key1", "val1", 0)

// get value under key1
r, ok := cache.Get("key1")
Expand All @@ -213,11 +214,12 @@ func ExampleLoadingCache() {

// get value under key1 after key expiration
r, ok = cache.Get("key1")
// don't convert to string as with ok == false value vould be nil
// don't convert to string as with ok == false value would be nil
fmt.Printf("value after expiration is found: %v, value: %v\n", ok, r)

// set value under key2, would evict old entry because it is already expired
cache.Set("key2", "val2")
// set value under key2, would evict old entry because it is already expired.
// ttl (last parameter) overrides cache-wide ttl.
cache.Set("key2", "val2", time.Minute*5)

fmt.Printf("%+v\n", cache)
// Output:
Expand Down

0 comments on commit b0beef9

Please sign in to comment.