Skip to content

Commit

Permalink
Add new function GetWithRefresh() (#64)
Browse files Browse the repository at this point in the history
* Add new function GetWithRefresh()

* Increase test coverage
  • Loading branch information
rockdaboot authored Nov 20, 2024
1 parent df0880f commit 3b73f42
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 2 deletions.
5 changes: 5 additions & 0 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ type Cache[K comparable, V any] interface {
// and the return value indicates that the key was not found.
Get(key K) (V, bool)

// GetAndRefresh returns the value associated with the key, setting it as the most
// recently used item.
// The lifetime of the found cache item is refreshed, even if it was already expired.
GetAndRefresh(key K, lifetime time.Duration) (V, bool)

// Peek looks up a key's value from the cache, without changing its recent-ness.
// If the found entry is already expired, the evict function is called.
Peek(key K) (V, bool)
Expand Down
44 changes: 43 additions & 1 deletion lru.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,26 @@ func (lru *LRU[K, V]) findKey(hash uint32, key K) (uint32, bool) {
}
}

func (lru *LRU[K, V]) findKeyNoExpire(hash uint32, key K) (uint32, bool) {
_, startPos := lru.hashToPos(hash)
if startPos == emptyBucket {
return emptyBucket, false
}

pos := startPos
for {
if key == lru.elements[pos].key {
return pos, true
}

pos = lru.elements[pos].nextBucket
if pos == startPos {
// Key not found
return emptyBucket, false
}
}
}

// Len returns the number of elements stored in the cache.
func (lru *LRU[K, V]) Len() int {
return int(lru.len)
Expand Down Expand Up @@ -448,6 +468,28 @@ func (lru *LRU[K, V]) get(hash uint32, key K) (value V, ok bool) {
return
}

// GetAndRefresh returns the value associated with the key, setting it as the most
// recently used item.
// The lifetime of the found cache item is refreshed, even if it was already expired.
func (lru *LRU[K, V]) GetAndRefresh(key K, lifetime time.Duration) (V, bool) {
return lru.getAndRefresh(lru.hash(key), key, lifetime)
}

func (lru *LRU[K, V]) getAndRefresh(hash uint32, key K, lifetime time.Duration) (value V, ok bool) {
if pos, ok := lru.findKeyNoExpire(hash, key); ok {
if pos != lru.head {
lru.unlinkElement(pos)
lru.setHead(pos)
}
lru.metrics.Hits++
lru.elements[pos].expire = expire(lifetime)
return lru.elements[pos].value, ok
}

lru.metrics.Misses++
return
}

// Peek looks up a key's value from the cache, without changing its recent-ness.
// If the found entry is already expired, the evict function is called.
func (lru *LRU[K, V]) Peek(key K) (value V, ok bool) {
Expand Down Expand Up @@ -482,7 +524,7 @@ func (lru *LRU[K, V]) Remove(key K) (removed bool) {
}

func (lru *LRU[K, V]) remove(hash uint32, key K) (removed bool) {
if pos, ok := lru.findKey(hash, key); ok {
if pos, ok := lru.findKeyNoExpire(hash, key); ok {
lru.removeAt(pos)
return ok
}
Expand Down
23 changes: 22 additions & 1 deletion lru_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,12 +247,33 @@ func TestSyncedLRU_AddWithExpire(t *testing.T) {
testCacheAddWithExpire(t, makeSyncedLRU(t, 2, nil))
}

func testCacheAddWithRefresh(t *testing.T, cache Cache[uint64, uint64]) {
cache.AddWithLifetime(1, 2, 100*time.Millisecond)
cache.AddWithLifetime(2, 3, 100*time.Millisecond)
_, ok := cache.Get(1)
FatalIf(t, !ok, "Failed to get")

time.Sleep(101 * time.Millisecond)
_, ok = cache.GetAndRefresh(1, 0)
FatalIf(t, !ok, "Unexpected expiration")
_, ok = cache.GetAndRefresh(2, 0)
FatalIf(t, !ok, "Unexpected expiration")
}

func TestLRU_AddWithRefresh(t *testing.T) {
testCacheAddWithRefresh(t, makeCache(t, 2, nil))
}

func TestSyncedLRU_AddWithRefresh(t *testing.T) {
testCacheAddWithRefresh(t, makeSyncedLRU(t, 2, nil))
}

func TestLRUMatch(t *testing.T) {
testCacheMatch(t, makeCache(t, 2, nil), 128)
}

func TestSyncedLRUMatch(t *testing.T) {
testCacheMatch(t, makeCache(t, 2, nil), 128)
testCacheMatch(t, makeSyncedLRU(t, 2, nil), 128)
}

// Test that Go map and the Cache stay in sync when adding
Expand Down
14 changes: 14 additions & 0 deletions shardedlru.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,20 @@ func (lru *ShardedLRU[K, V]) Get(key K) (value V, ok bool) {
return
}

// GetAndRefresh returns the value associated with the key, setting it as the most
// recently used item.
// The lifetime of the found cache item is refreshed, even if it was already expired.
func (lru *ShardedLRU[K, V]) GetAndRefresh(key K, lifetime time.Duration) (value V, ok bool) {
hash := lru.hash(key)
shard := (hash >> 16) & lru.mask

lru.mus[shard].Lock()
value, ok = lru.lrus[shard].getAndRefresh(hash, key, lifetime)
lru.mus[shard].Unlock()

return
}

// Peek looks up a key's value from the cache, without changing its recent-ness.
// If the found entry is already expired, the evict function is called.
func (lru *ShardedLRU[K, V]) Peek(key K) (value V, ok bool) {
Expand Down
1 change: 1 addition & 0 deletions shardedlru_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func TestShardedRaceCondition(t *testing.T) {
call(func() { _ = lru.AddWithLifetime(1, 1, 0) })
call(func() { _ = lru.Add(1, 1) })
call(func() { _, _ = lru.Get(1) })
call(func() { _, _ = lru.GetAndRefresh(1, 0) })
call(func() { _, _ = lru.Peek(1) })
call(func() { _ = lru.Contains(1) })
call(func() { _ = lru.Remove(1) })
Expand Down
13 changes: 13 additions & 0 deletions syncedlru.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,19 @@ func (lru *SyncedLRU[K, V]) Get(key K) (value V, ok bool) {
return
}

// GetAndRefresh returns the value associated with the key, setting it as the most
// recently used item.
// The lifetime of the found cache item is refreshed, even if it was already expired.
func (lru *SyncedLRU[K, V]) GetAndRefresh(key K, lifetime time.Duration) (value V, ok bool) {
hash := lru.lru.hash(key)

lru.mu.Lock()
value, ok = lru.lru.getAndRefresh(hash, key, lifetime)
lru.mu.Unlock()

return
}

// Peek looks up a key's value from the cache, without changing its recent-ness.
// If the found entry is already expired, the evict function is called.
func (lru *SyncedLRU[K, V]) Peek(key K) (value V, ok bool) {
Expand Down
1 change: 1 addition & 0 deletions syncedlru_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func TestSyncedRaceCondition(t *testing.T) {
call(func() { _ = lru.AddWithLifetime(1, 1, 0) })
call(func() { _ = lru.Add(1, 1) })
call(func() { _, _ = lru.Get(1) })
call(func() { _, _ = lru.GetAndRefresh(1, 0) })
call(func() { _, _ = lru.Peek(1) })
call(func() { _ = lru.Contains(1) })
call(func() { _ = lru.Remove(1) })
Expand Down

0 comments on commit 3b73f42

Please sign in to comment.