freelru: Add PeekWithLifetime and UpdateLifetime

This commit is contained in:
世界 2024-11-26 11:29:14 +08:00
parent c8f251c668
commit 3613ead480
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
4 changed files with 117 additions and 23 deletions

View file

@ -55,6 +55,10 @@ type Cache[K comparable, V any] interface {
// If the found entry is already expired, the evict function is called.
Peek(key K) (V, bool)
PeekWithLifetime(key K) (value V, lifetime time.Time, ok bool)
UpdateLifetime(key K, value V, lifetime time.Duration) bool
// Contains checks for the existence of a key, without changing its recent-ness.
// If the found entry is already expired, the evict function is called.
Contains(key K) bool

View file

@ -62,7 +62,7 @@ type element[K comparable, V any] struct {
const emptyBucket = math.MaxUint32
// LRU implements a non-thread safe fixed size LRU cache.
type LRU[K comparable, V any] struct {
type LRU[K comparable, V comparable] struct {
buckets []uint32 // contains positions of bucket lists or 'emptyBucket'
elements []element[K, V]
onEvict OnEvictCallback[K, V]
@ -122,7 +122,7 @@ func (lru *LRU[K, V]) SetHealthCheck(healthCheck HealthCheckCallback[K, V]) {
// New constructs an LRU with the given capacity of elements.
// The hash function calculates a hash value from the keys.
func New[K comparable, V any](capacity uint32, hash HashKeyCallback[K]) (*LRU[K, V], error) {
func New[K comparable, V comparable](capacity uint32, hash HashKeyCallback[K]) (*LRU[K, V], error) {
return NewWithSize[K, V](capacity, capacity, hash)
}
@ -131,7 +131,7 @@ func New[K comparable, V any](capacity uint32, hash HashKeyCallback[K]) (*LRU[K,
// A size greater than the capacity increases memory consumption and decreases the CPU consumption
// by reducing the chance of collisions.
// Size must not be lower than the capacity.
func NewWithSize[K comparable, V any](capacity, size uint32, hash HashKeyCallback[K]) (
func NewWithSize[K comparable, V comparable](capacity, size uint32, hash HashKeyCallback[K]) (
*LRU[K, V], error,
) {
if capacity == 0 {
@ -156,7 +156,7 @@ func NewWithSize[K comparable, V any](capacity, size uint32, hash HashKeyCallbac
return &lru, nil
}
func initLRU[K comparable, V any](lru *LRU[K, V], capacity, size uint32, hash HashKeyCallback[K],
func initLRU[K comparable, V comparable](lru *LRU[K, V], capacity, size uint32, hash HashKeyCallback[K],
buckets []uint32, elements []element[K, V],
) {
lru.cap = capacity
@ -471,26 +471,66 @@ func (lru *LRU[K, V]) get(hash uint32, key K) (value V, expire int64, ok 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.
func (lru *LRU[K, V]) Peek(key K) (value V, ok bool) {
return lru.peek(lru.hash(key), key)
value, _, ok = lru.peek(lru.hash(key), key)
return
}
func (lru *LRU[K, V]) peek(hash uint32, key K) (value V, ok bool) {
if pos, _, ok := lru.findKey(hash, key, false); ok {
return lru.elements[pos].value, ok
func (lru *LRU[K, V]) PeekWithLifetime(key K) (value V, lifetime time.Time, ok bool) {
value, expireMills, ok := lru.peek(lru.hash(key), key)
lifetime = time.UnixMilli(expireMills)
return
}
func (lru *LRU[K, V]) peek(hash uint32, key K) (value V, expire int64, ok bool) {
if pos, expireMills, ok := lru.findKey(hash, key, false); ok {
return lru.elements[pos].value, expireMills, ok
}
return
}
func (lru *LRU[K, V]) UpdateLifetime(key K, value V, lifetime time.Duration) bool {
return lru.updateLifetime(lru.hash(key), key, value, lifetime)
}
func (lru *LRU[K, V]) updateLifetime(hash uint32, key K, value V, lifetime time.Duration) bool {
_, startPos := lru.hashToPos(hash)
if startPos == emptyBucket {
return false
}
pos := startPos
for {
if lru.elements[pos].key == key {
if lru.elements[pos].value != value {
return false
}
lru.elements[pos].expire = expire(lifetime)
if pos != lru.head {
lru.unlinkElement(pos)
lru.setHead(pos)
}
lru.metrics.Inserts++
return true
}
pos = lru.elements[pos].nextBucket
if pos == startPos {
return false
}
}
}
// Contains checks for the existence of a key, without changing its recent-ness.
// If the found entry is already expired, the evict function is called.
func (lru *LRU[K, V]) Contains(key K) (ok bool) {
_, ok = lru.peek(lru.hash(key), key)
_, _, ok = lru.peek(lru.hash(key), key)
return
}
func (lru *LRU[K, V]) contains(hash uint32, key K) (ok bool) {
_, ok = lru.peek(hash, key)
_, _, ok = lru.peek(hash, key)
return
}

View file

@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/require"
)
func TestMyChange0(t *testing.T) {
func TestUpdateLifetimeOnGet(t *testing.T) {
t.Parallel()
lru, err := freelru.New[string, string](1024, maphash.NewHasher[string]().Hash32)
require.NoError(t, err)
@ -24,7 +24,7 @@ func TestMyChange0(t *testing.T) {
require.True(t, ok)
}
func TestMyChange1(t *testing.T) {
func TestUpdateLifetimeOnGet1(t *testing.T) {
t.Parallel()
lru, err := freelru.New[string, string](1024, maphash.NewHasher[string]().Hash32)
require.NoError(t, err)
@ -36,3 +36,43 @@ func TestMyChange1(t *testing.T) {
_, ok := lru.Get("hello")
require.False(t, ok)
}
func TestUpdateLifetime(t *testing.T) {
t.Parallel()
lru, err := freelru.New[string, string](1024, maphash.NewHasher[string]().Hash32)
require.NoError(t, err)
lru.Add("hello", "world")
require.True(t, lru.UpdateLifetime("hello", "world", 2*time.Second))
time.Sleep(time.Second)
_, ok := lru.Get("hello")
require.True(t, ok)
time.Sleep(time.Second + time.Millisecond*100)
_, ok = lru.Get("hello")
require.False(t, ok)
}
func TestUpdateLifetime1(t *testing.T) {
t.Parallel()
lru, err := freelru.New[string, string](1024, maphash.NewHasher[string]().Hash32)
require.NoError(t, err)
lru.Add("hello", "world")
require.False(t, lru.UpdateLifetime("hello", "not world", 2*time.Second))
time.Sleep(2*time.Second + time.Millisecond*100)
_, ok := lru.Get("hello")
require.True(t, ok)
}
func TestUpdateLifetime2(t *testing.T) {
t.Parallel()
lru, err := freelru.New[string, string](1024, maphash.NewHasher[string]().Hash32)
require.NoError(t, err)
lru.AddWithLifetime("hello", "world", 2*time.Second)
time.Sleep(time.Second)
require.True(t, lru.UpdateLifetime("hello", "world", 2*time.Second))
time.Sleep(time.Second + time.Millisecond*100)
_, ok := lru.Get("hello")
require.True(t, ok)
time.Sleep(time.Second + time.Millisecond*100)
_, ok = lru.Get("hello")
require.False(t, ok)
}

View file

@ -12,7 +12,7 @@ import (
// ShardedLRU is a thread-safe, sharded, fixed size LRU cache.
// Sharding is used to reduce lock contention on high concurrency.
// The downside is that exact LRU behavior is not given (as for the LRU and SynchedLRU types).
type ShardedLRU[K comparable, V any] struct {
type ShardedLRU[K comparable, V comparable] struct {
lrus []LRU[K, V]
mus []sync.RWMutex
hash HashKeyCallback[K]
@ -66,7 +66,7 @@ func nextPowerOfTwo(val uint32) uint32 {
}
// NewSharded creates a new thread-safe sharded LRU hashmap with the given capacity.
func NewSharded[K comparable, V any](capacity uint32, hash HashKeyCallback[K]) (*ShardedLRU[K, V],
func NewSharded[K comparable, V comparable](capacity uint32, hash HashKeyCallback[K]) (*ShardedLRU[K, V],
error,
) {
size := uint32(float64(capacity) * 1.25) // 25% extra space for fewer collisions
@ -74,7 +74,7 @@ func NewSharded[K comparable, V any](capacity uint32, hash HashKeyCallback[K]) (
return NewShardedWithSize[K, V](uint32(runtime.GOMAXPROCS(0)*16), capacity, size, hash)
}
func NewShardedWithSize[K comparable, V any](shards, capacity, size uint32,
func NewShardedWithSize[K comparable, V comparable](shards, capacity, size uint32,
hash HashKeyCallback[K]) (
*ShardedLRU[K, V], error,
) {
@ -174,13 +174,7 @@ func (lru *ShardedLRU[K, V]) Add(key K, value V) (evicted bool) {
// If the found cache item is already expired, the evict function is called
// and the return value indicates that the key was not found.
func (lru *ShardedLRU[K, V]) Get(key K) (value V, ok bool) {
hash := lru.hash(key)
shard := (hash >> 16) & lru.mask
lru.mus[shard].Lock()
value, _, ok = lru.lrus[shard].get(hash, key)
lru.mus[shard].Unlock()
value, _, ok = lru.GetWithLifetime(key)
return
}
@ -198,11 +192,27 @@ func (lru *ShardedLRU[K, V]) GetWithLifetime(key K) (value V, lifetime time.Time
// 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) {
value, _, ok = lru.PeekWithLifetime(key)
return
}
func (lru *ShardedLRU[K, V]) PeekWithLifetime(key K) (value V, lifetime time.Time, ok bool) {
hash := lru.hash(key)
shard := (hash >> 16) & lru.mask
lru.mus[shard].Lock()
value, ok = lru.lrus[shard].peek(hash, key)
value, expireMills, ok := lru.lrus[shard].peek(hash, key)
lru.mus[shard].Unlock()
lifetime = time.UnixMilli(expireMills)
return
}
func (lru *ShardedLRU[K, V]) UpdateLifetime(key K, value V, lifetime time.Duration) (ok bool) {
hash := lru.hash(key)
shard := (hash >> 16) & lru.mask
lru.mus[shard].Lock()
ok = lru.lrus[shard].updateLifetime(hash, key, value, lifetime)
lru.mus[shard].Unlock()
return