mirror of
https://github.com/SagerNet/sing.git
synced 2025-04-02 03:17:37 +03:00
freelru: Add PeekWithLifetime
and UpdateLifetime
This commit is contained in:
parent
c8f251c668
commit
3613ead480
4 changed files with 117 additions and 23 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue