mirror of
https://github.com/luxfi/cache.git
synced 2026-09-24 00:24:18 +00:00
feat: add LRU and DualMapCache types, update to metric v1.4.10
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
// Copyright (C) 2026, Lux Partners Limited. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/luxfi/metric"
|
||||
)
|
||||
|
||||
// DualMapCache is a simple two-map cache placeholder with migration hooks.
|
||||
// The implementation is intentionally minimal to preserve API compatibility.
|
||||
type DualMapCache[K comparable, V any] struct {
|
||||
mu sync.RWMutex
|
||||
items map[K]V
|
||||
}
|
||||
|
||||
// NewDualMapCache creates a new DualMapCache. Metrics are optional.
|
||||
func NewDualMapCache[K comparable, V any](_ *metric.MetricsRegistry) *DualMapCache[K, V] {
|
||||
return &DualMapCache[K, V]{
|
||||
items: make(map[K]V),
|
||||
}
|
||||
}
|
||||
|
||||
// Put inserts or replaces an element in the cache.
|
||||
func (c *DualMapCache[K, V]) Put(key K, value V) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.items[key] = value
|
||||
}
|
||||
|
||||
// Get returns the entry with the key, if it exists.
|
||||
func (c *DualMapCache[K, V]) Get(key K) (V, bool) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
val, ok := c.items[key]
|
||||
return val, ok
|
||||
}
|
||||
|
||||
// Evict removes the specified entry from the cache.
|
||||
func (c *DualMapCache[K, V]) Evict(key K) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
delete(c.items, key)
|
||||
}
|
||||
|
||||
// Flush removes all entries from the cache.
|
||||
func (c *DualMapCache[K, V]) Flush() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.items = make(map[K]V)
|
||||
}
|
||||
|
||||
// Len returns the number of elements in the cache.
|
||||
func (c *DualMapCache[K, V]) Len() int {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return len(c.items)
|
||||
}
|
||||
|
||||
// PortionFilled returns fraction of cache currently filled.
|
||||
func (c *DualMapCache[K, V]) PortionFilled() float64 {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
if len(c.items) == 0 {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
// Migrate is a no-op placeholder for dual-map cache migration.
|
||||
func (c *DualMapCache[K, V]) Migrate() {}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package cache
|
||||
|
||||
var _ Cacher[struct{}, struct{}] = (*Empty[struct{}, struct{}])(nil)
|
||||
|
||||
// Empty is a cache that doesn't store anything.
|
||||
type Empty[K any, V any] struct{}
|
||||
|
||||
func (*Empty[K, V]) Put(K, V) {}
|
||||
|
||||
func (*Empty[K, V]) Get(K) (V, bool) {
|
||||
var zero V
|
||||
return zero, false
|
||||
}
|
||||
|
||||
func (*Empty[K, _]) Evict(K) {}
|
||||
|
||||
func (*Empty[_, _]) Flush() {}
|
||||
|
||||
func (*Empty[_, _]) Len() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (*Empty[_, _]) PortionFilled() float64 {
|
||||
return 0
|
||||
}
|
||||
@@ -2,20 +2,34 @@ module github.com/luxfi/cache
|
||||
|
||||
go 1.25.5
|
||||
|
||||
require github.com/luxfi/metric v1.4.5
|
||||
require (
|
||||
github.com/luxfi/container v0.0.2
|
||||
github.com/luxfi/math v1.2.3
|
||||
github.com/luxfi/metric v1.4.10
|
||||
github.com/stretchr/testify v1.11.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/gorilla/rpc v1.2.1 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/luxfi/crypto v1.17.36 // indirect
|
||||
github.com/luxfi/ids v1.2.7 // indirect
|
||||
github.com/luxfi/math/big v0.1.0 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.4 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -2,18 +2,37 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/gorilla/rpc v1.2.1 h1:yC+LMV5esttgpVvNORL/xX4jvTTEUE30UZhZ5JF7K9k=
|
||||
github.com/gorilla/rpc v1.2.1/go.mod h1:uNpOihAlF5xRFLuTYhfR0yfCTm0WTQSQttkMSptRfGk=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/luxfi/container v0.0.2 h1:UNt3C5LGlkxU1OJeQUoxRA0AYO0tG35x1ieY9vHdon0=
|
||||
github.com/luxfi/container v0.0.2/go.mod h1:nnK+TxHmGry0mGnfun4jx0tLqWJgV/GbZXTETMuFJn4=
|
||||
github.com/luxfi/crypto v1.17.36 h1:fN/m5oF2kd44/5JXSILnBxjm3AmgCTw8qFwfkdJaAxg=
|
||||
github.com/luxfi/crypto v1.17.36/go.mod h1:UHzltsXPDF/tGcmTiL/DbkClAflr3YmvOaXlwWC6eXo=
|
||||
github.com/luxfi/ids v1.2.7 h1:3B42EbzR2cdY3veo1yOFOOIeEE+HULnCye2Ye32nXqI=
|
||||
github.com/luxfi/ids v1.2.7/go.mod h1:svLsj7e6ixJVfRYaQqv9RWjBIW11Vz738+d08BVpeCg=
|
||||
github.com/luxfi/math v1.2.3 h1:BgvIFw/srPXFLbcqtoDhLJOfmBsn86GPA1iWgsoyUb4=
|
||||
github.com/luxfi/math v1.2.3/go.mod h1:C8STnF2H+D6rqBPt248CiWY2TGuJgdtv/+4UqrT15iM=
|
||||
github.com/luxfi/math/big v0.1.0 h1:Vz4c0RsZVPdIKPsHPgAJChH/R3p15WHRUz7LkLf+NIQ=
|
||||
github.com/luxfi/math/big v0.1.0/go.mod h1:BuxSu22RbO93xBLk5Eam5nldFponoJ73xDFz4uJ3Huk=
|
||||
github.com/luxfi/metric v1.4.5 h1:fD6gtpzB5ebGB06/m4K7e5+tOV5RiVHSZCV+Zb8ZDxk=
|
||||
github.com/luxfi/metric v1.4.5/go.mod h1:AUQ7NSNz9WndAcr/SKnOkP7XSFFnBXOa+ihtJYfDaQY=
|
||||
github.com/luxfi/metric v1.4.10 h1:uWeLupMLxK7YK1c99gY23cT+hornWg7OmZgHNK4HNbw=
|
||||
github.com/luxfi/metric v1.4.10/go.mod h1:3OIQ+e6mciesm3cik3Q4CSOAvTlBy4JA0ZoKzCB9APo=
|
||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
@@ -22,8 +41,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
|
||||
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
|
||||
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
@@ -34,6 +53,10 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
|
||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Package lru provides the ONE standard LRU cache implementation using container package.
|
||||
package lru
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/luxfi/cache"
|
||||
"github.com/luxfi/container"
|
||||
)
|
||||
|
||||
// Cache is the standard LRU cache - ONE implementation, no duplicates
|
||||
type Cache[K comparable, V any] struct {
|
||||
mu sync.Mutex
|
||||
containerCache container.Cache[K, V] // Uses container package internally
|
||||
capacity int
|
||||
onEvict func(K, V)
|
||||
}
|
||||
|
||||
// NewCache creates a new LRU cache - THE standard way
|
||||
func NewCache[K comparable, V any](size int) *Cache[K, V] {
|
||||
if size <= 0 {
|
||||
size = 1
|
||||
}
|
||||
return &Cache[K, V]{
|
||||
containerCache: container.NewLRUCache[K, V](size),
|
||||
capacity: size,
|
||||
onEvict: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCacheWithOnEvict creates cache with eviction callback
|
||||
func NewCacheWithOnEvict[K comparable, V any](size int, onEvict func(K, V)) *Cache[K, V] {
|
||||
if size <= 0 {
|
||||
size = 1
|
||||
}
|
||||
return &Cache[K, V]{
|
||||
containerCache: container.NewLRUCacheWithOnEvict[K, V](size, onEvict),
|
||||
capacity: size,
|
||||
onEvict: onEvict,
|
||||
}
|
||||
}
|
||||
|
||||
// Get retrieves value from cache
|
||||
func (c *Cache[K, V]) Get(key K) (value V, ok bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.containerCache.Get(key)
|
||||
}
|
||||
|
||||
// Put adds value to cache
|
||||
func (c *Cache[K, V]) Put(key K, value V) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.containerCache.Put(key, value)
|
||||
}
|
||||
|
||||
// Delete removes value from cache
|
||||
func (c *Cache[K, V]) Delete(key K) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.containerCache.Delete(key)
|
||||
}
|
||||
|
||||
// Len returns cache size
|
||||
func (c *Cache[K, V]) Len() int {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.containerCache.Len()
|
||||
}
|
||||
|
||||
// Clear removes all items
|
||||
func (c *Cache[K, V]) Clear() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.containerCache = container.NewLRUCache[K, V](c.capacity)
|
||||
}
|
||||
|
||||
// Contains checks key existence
|
||||
func (c *Cache[K, V]) Contains(key K) bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
_, ok := c.containerCache.Get(key)
|
||||
return ok
|
||||
}
|
||||
|
||||
// Size returns cache size
|
||||
func (c *Cache[K, V]) Size() int {
|
||||
return c.Len()
|
||||
}
|
||||
|
||||
// Evict removes a key from cache (required by Cacher interface)
|
||||
func (c *Cache[K, V]) Evict(key K) {
|
||||
c.Delete(key)
|
||||
}
|
||||
|
||||
// Flush removes all entries from cache (required by Cacher interface)
|
||||
func (c *Cache[K, V]) Flush() {
|
||||
c.Clear()
|
||||
}
|
||||
|
||||
// PortionFilled returns fraction of cache currently filled (0 --> 1)
|
||||
func (c *Cache[K, V]) PortionFilled() float64 {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
current := float64(c.Len())
|
||||
capacity := float64(c.capacity)
|
||||
if capacity == 0 {
|
||||
return 0
|
||||
}
|
||||
return current / capacity
|
||||
}
|
||||
|
||||
// Interface compliance
|
||||
var _ cache.Cacher[struct{}, struct{}] = (*Cache[struct{}, struct{}])(nil)
|
||||
@@ -0,0 +1,51 @@
|
||||
package lru
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestContainerCache(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
cache := NewCache[string, string](3)
|
||||
|
||||
// Test basic operations
|
||||
cache.Put("a", "apple")
|
||||
cache.Put("b", "banana")
|
||||
cache.Put("c", "cherry")
|
||||
|
||||
require.Equal(3, cache.Len())
|
||||
require.Equal(1.0, cache.PortionFilled())
|
||||
|
||||
// Test Get
|
||||
val, ok := cache.Get("a")
|
||||
require.True(ok)
|
||||
require.Equal("apple", val)
|
||||
|
||||
// Test eviction
|
||||
cache.Put("d", "date")
|
||||
require.Equal(3, cache.Len()) // Should still be 3 after eviction
|
||||
|
||||
// Test Flush
|
||||
cache.Flush()
|
||||
require.Equal(0, cache.Len())
|
||||
require.Equal(0.0, cache.PortionFilled())
|
||||
}
|
||||
|
||||
func TestCacheWithEvictionCallback(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
evicted := make([]string, 0)
|
||||
cache := NewCacheWithOnEvict[string, string](2, func(k, v string) {
|
||||
evicted = append(evicted, k)
|
||||
})
|
||||
|
||||
cache.Put("x", "value-x")
|
||||
cache.Put("y", "value-y")
|
||||
cache.Put("z", "value-z") // Should evict 'x'
|
||||
|
||||
require.Len(evicted, 1)
|
||||
require.Equal("x", evicted[0])
|
||||
}
|
||||
-94
@@ -1,94 +0,0 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Package lru provides LRU cache implementations.
|
||||
package lru
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/luxfi/cache"
|
||||
"github.com/luxfi/utils"
|
||||
"github.com/luxfi/utils/linked"
|
||||
)
|
||||
|
||||
var _ cache.Cacher[struct{}, struct{}] = (*Cache[struct{}, struct{}])(nil)
|
||||
|
||||
// Cache is a key value store with bounded size. If the size is attempted to be
|
||||
// exceeded, then an element is removed from the cache before the insertion is
|
||||
// done, based on evicting the least recently used value.
|
||||
type Cache[K comparable, V any] struct {
|
||||
lock sync.Mutex
|
||||
elements *linked.Hashmap[K, V]
|
||||
size int
|
||||
|
||||
// onEvict is called with the key and value of an entry before eviction.
|
||||
onEvict func(K, V)
|
||||
}
|
||||
|
||||
func NewCache[K comparable, V any](size int) *Cache[K, V] {
|
||||
return NewCacheWithOnEvict(size, func(K, V) {})
|
||||
}
|
||||
|
||||
// NewCacheWithOnEvict creates a new LRU cache with the given size and eviction callback.
|
||||
func NewCacheWithOnEvict[K comparable, V any](size int, onEvict func(K, V)) *Cache[K, V] {
|
||||
return &Cache[K, V]{
|
||||
elements: linked.NewHashmap[K, V](),
|
||||
size: max(size, 1),
|
||||
onEvict: onEvict,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache[K, V]) Put(key K, value V) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
if c.elements.Len() == c.size {
|
||||
oldestKey, oldestVal, _ := c.elements.Oldest()
|
||||
c.elements.Delete(oldestKey)
|
||||
if c.onEvict != nil {
|
||||
c.onEvict(oldestKey, oldestVal)
|
||||
}
|
||||
}
|
||||
c.elements.Put(key, value)
|
||||
}
|
||||
|
||||
func (c *Cache[K, V]) Get(key K) (V, bool) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
val, ok := c.elements.Get(key)
|
||||
if !ok {
|
||||
return utils.Zero[V](), false
|
||||
}
|
||||
c.elements.Put(key, val) // Mark [k] as MRU.
|
||||
return val, true
|
||||
}
|
||||
|
||||
func (c *Cache[K, _]) Evict(key K) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
c.elements.Delete(key)
|
||||
}
|
||||
|
||||
func (c *Cache[_, _]) Flush() {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
c.elements.Clear()
|
||||
}
|
||||
|
||||
func (c *Cache[_, _]) Len() int {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
return c.elements.Len()
|
||||
}
|
||||
|
||||
func (c *Cache[_, _]) PortionFilled() float64 {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
return float64(c.elements.Len()) / float64(c.size)
|
||||
}
|
||||
+92
-97
@@ -1,140 +1,135 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// Copyright (C) 2026, Lux Partners Limited. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package lru
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"sync"
|
||||
|
||||
"github.com/luxfi/cache"
|
||||
"github.com/luxfi/utils"
|
||||
"github.com/luxfi/utils/linked"
|
||||
)
|
||||
|
||||
var _ cache.Cacher[struct{}, any] = (*SizedCache[struct{}, any])(nil)
|
||||
// SizedCache is an LRU cache bounded by total size rather than entry count.
|
||||
type SizedCache[K comparable, V any] struct {
|
||||
mu sync.Mutex
|
||||
maxSize int
|
||||
currentSize int
|
||||
sizeFn func(K, V) int
|
||||
items map[K]*list.Element
|
||||
lru *list.List
|
||||
}
|
||||
|
||||
// sizedElement is used to store the element with its size, so we don't
|
||||
// calculate the size multiple times.
|
||||
//
|
||||
// This ensures that any inconsistencies returned by the size function can not
|
||||
// corrupt the cache.
|
||||
type sizedElement[V any] struct {
|
||||
type sizedEntry[K comparable, V any] struct {
|
||||
key K
|
||||
value V
|
||||
size int
|
||||
}
|
||||
|
||||
// SizedCache is a key value store with bounded size. If the size is attempted
|
||||
// to be exceeded, then elements are removed from the cache until the bound is
|
||||
// honored, based on evicting the least recently used value.
|
||||
type SizedCache[K comparable, V any] struct {
|
||||
lock sync.Mutex
|
||||
elements *linked.Hashmap[K, *sizedElement[V]]
|
||||
maxSize int
|
||||
currentSize int
|
||||
size func(K, V) int
|
||||
}
|
||||
|
||||
func NewSizedCache[K comparable, V any](maxSize int, size func(K, V) int) *SizedCache[K, V] {
|
||||
// NewSizedCache creates a size-bounded LRU cache.
|
||||
func NewSizedCache[K comparable, V any](maxSize int, sizeFn func(K, V) int) *SizedCache[K, V] {
|
||||
if maxSize <= 0 {
|
||||
maxSize = 1
|
||||
}
|
||||
if sizeFn == nil {
|
||||
sizeFn = func(K, V) int { return 1 }
|
||||
}
|
||||
return &SizedCache[K, V]{
|
||||
elements: linked.NewHashmap[K, *sizedElement[V]](),
|
||||
maxSize: maxSize,
|
||||
size: size,
|
||||
maxSize: maxSize,
|
||||
sizeFn: sizeFn,
|
||||
items: make(map[K]*list.Element),
|
||||
lru: list.New(),
|
||||
}
|
||||
}
|
||||
|
||||
// Put inserts or replaces a value.
|
||||
func (c *SizedCache[K, V]) Put(key K, value V) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.put(key, value)
|
||||
}
|
||||
|
||||
func (c *SizedCache[K, V]) Get(key K) (V, bool) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
return c.get(key)
|
||||
}
|
||||
|
||||
func (c *SizedCache[K, V]) Evict(key K) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
c.evict(key)
|
||||
}
|
||||
|
||||
func (c *SizedCache[K, V]) Flush() {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
c.flush()
|
||||
}
|
||||
|
||||
func (c *SizedCache[_, _]) Len() int {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
return c.len()
|
||||
}
|
||||
|
||||
func (c *SizedCache[_, _]) PortionFilled() float64 {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
return c.portionFilled()
|
||||
}
|
||||
|
||||
func (c *SizedCache[K, V]) put(key K, value V) {
|
||||
newEntrySize := c.size(key, value)
|
||||
if newEntrySize > c.maxSize {
|
||||
c.flush()
|
||||
entrySize := c.sizeFn(key, value)
|
||||
if entrySize > c.maxSize {
|
||||
c.flushLocked()
|
||||
return
|
||||
}
|
||||
|
||||
if oldElement, ok := c.elements.Get(key); ok {
|
||||
c.currentSize -= oldElement.size
|
||||
if elem, ok := c.items[key]; ok {
|
||||
oldEntry := elem.Value.(*sizedEntry[K, V])
|
||||
c.currentSize -= oldEntry.size
|
||||
c.lru.Remove(elem)
|
||||
delete(c.items, key)
|
||||
}
|
||||
|
||||
// Remove elements until the size of elements in the cache <= [c.maxSize].
|
||||
for c.currentSize > c.maxSize-newEntrySize {
|
||||
oldestKey, oldestElement, _ := c.elements.Oldest()
|
||||
c.elements.Delete(oldestKey)
|
||||
c.currentSize -= oldestElement.size
|
||||
for c.currentSize > c.maxSize-entrySize {
|
||||
back := c.lru.Back()
|
||||
if back == nil {
|
||||
break
|
||||
}
|
||||
oldEntry := back.Value.(*sizedEntry[K, V])
|
||||
c.currentSize -= oldEntry.size
|
||||
delete(c.items, oldEntry.key)
|
||||
c.lru.Remove(back)
|
||||
}
|
||||
|
||||
c.elements.Put(key, &sizedElement[V]{
|
||||
value: value,
|
||||
size: newEntrySize,
|
||||
})
|
||||
c.currentSize += newEntrySize
|
||||
e := &sizedEntry[K, V]{key: key, value: value, size: entrySize}
|
||||
c.items[key] = c.lru.PushFront(e)
|
||||
c.currentSize += entrySize
|
||||
}
|
||||
|
||||
func (c *SizedCache[K, V]) get(key K) (V, bool) {
|
||||
element, ok := c.elements.Get(key)
|
||||
if !ok {
|
||||
return utils.Zero[V](), false
|
||||
}
|
||||
// Get retrieves a value and marks it as most recently used.
|
||||
func (c *SizedCache[K, V]) Get(key K) (V, bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.elements.Put(key, element) // Mark [k] as MRU.
|
||||
return element.value, true
|
||||
if elem, ok := c.items[key]; ok {
|
||||
c.lru.MoveToFront(elem)
|
||||
return elem.Value.(*sizedEntry[K, V]).value, true
|
||||
}
|
||||
var zero V
|
||||
return zero, false
|
||||
}
|
||||
|
||||
func (c *SizedCache[K, _]) evict(key K) {
|
||||
if element, ok := c.elements.Get(key); ok {
|
||||
c.elements.Delete(key)
|
||||
c.currentSize -= element.size
|
||||
// Evict removes a key from the cache.
|
||||
func (c *SizedCache[K, V]) Evict(key K) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if elem, ok := c.items[key]; ok {
|
||||
entry := elem.Value.(*sizedEntry[K, V])
|
||||
c.currentSize -= entry.size
|
||||
delete(c.items, key)
|
||||
c.lru.Remove(elem)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *SizedCache[K, V]) flush() {
|
||||
c.elements.Clear()
|
||||
// Flush removes all entries.
|
||||
func (c *SizedCache[K, V]) Flush() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.flushLocked()
|
||||
}
|
||||
|
||||
func (c *SizedCache[K, V]) flushLocked() {
|
||||
c.items = make(map[K]*list.Element)
|
||||
c.lru.Init()
|
||||
c.currentSize = 0
|
||||
}
|
||||
|
||||
func (c *SizedCache[_, _]) len() int {
|
||||
return c.elements.Len()
|
||||
// Len returns number of entries.
|
||||
func (c *SizedCache[K, V]) Len() int {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return len(c.items)
|
||||
}
|
||||
|
||||
func (c *SizedCache[_, _]) portionFilled() float64 {
|
||||
// PortionFilled returns the ratio of size used to max size.
|
||||
func (c *SizedCache[K, V]) PortionFilled() float64 {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.maxSize == 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(c.currentSize) / float64(c.maxSize)
|
||||
}
|
||||
|
||||
var _ cache.Cacher[struct{}, struct{}] = (*SizedCache[struct{}, struct{}])(nil)
|
||||
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/luxfi/math/linked"
|
||||
)
|
||||
|
||||
var _ Cacher[struct{}, struct{}] = (*LRU[struct{}, struct{}])(nil)
|
||||
|
||||
// NewLRU creates a new LRU cache with the specified size.
|
||||
func NewLRU[K comparable, V any](size int) *LRU[K, V] {
|
||||
return &LRU[K, V]{
|
||||
Size: size,
|
||||
}
|
||||
}
|
||||
|
||||
// LRU is a key value store with bounded size. If the size is attempted to be
|
||||
// exceeded, then an element is removed from the cache before the insertion is
|
||||
// done, based on evicting the least recently used value.
|
||||
type LRU[K comparable, V any] struct {
|
||||
lock sync.Mutex
|
||||
elements *linked.Hashmap[K, V]
|
||||
// If set to <= 0, will be set internally to 1.
|
||||
Size int
|
||||
}
|
||||
|
||||
func (c *LRU[K, V]) Put(key K, value V) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
c.put(key, value)
|
||||
}
|
||||
|
||||
func (c *LRU[K, V]) Get(key K) (V, bool) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
return c.get(key)
|
||||
}
|
||||
|
||||
func (c *LRU[K, _]) Evict(key K) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
c.evict(key)
|
||||
}
|
||||
|
||||
func (c *LRU[_, _]) Flush() {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
c.flush()
|
||||
}
|
||||
|
||||
func (c *LRU[_, _]) Len() int {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
return c.len()
|
||||
}
|
||||
|
||||
func (c *LRU[_, _]) PortionFilled() float64 {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
return c.portionFilled()
|
||||
}
|
||||
|
||||
func (c *LRU[K, V]) put(key K, value V) {
|
||||
c.resize()
|
||||
|
||||
if c.elements.Len() == c.Size {
|
||||
oldestKey, _, _ := c.elements.Oldest()
|
||||
c.elements.Delete(oldestKey)
|
||||
}
|
||||
c.elements.Put(key, value)
|
||||
}
|
||||
|
||||
func (c *LRU[K, V]) get(key K) (V, bool) {
|
||||
c.resize()
|
||||
|
||||
val, ok := c.elements.Get(key)
|
||||
if !ok {
|
||||
var zero V
|
||||
return zero, false
|
||||
}
|
||||
c.elements.Put(key, val) // Mark [k] as MRU.
|
||||
return val, true
|
||||
}
|
||||
|
||||
func (c *LRU[K, _]) evict(key K) {
|
||||
c.resize()
|
||||
|
||||
c.elements.Delete(key)
|
||||
}
|
||||
|
||||
func (c *LRU[K, V]) flush() {
|
||||
if c.elements != nil {
|
||||
c.elements.Clear()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *LRU[_, _]) len() int {
|
||||
if c.elements == nil {
|
||||
return 0
|
||||
}
|
||||
return c.elements.Len()
|
||||
}
|
||||
|
||||
func (c *LRU[_, _]) portionFilled() float64 {
|
||||
return float64(c.len()) / float64(c.Size)
|
||||
}
|
||||
|
||||
// Initializes [c.elements] if it's nil.
|
||||
// Sets [c.size] to 1 if it's <= 0.
|
||||
// Removes oldest elements to make number of elements
|
||||
// in the cache == [c.size] if necessary.
|
||||
func (c *LRU[K, V]) resize() {
|
||||
if c.elements == nil {
|
||||
c.elements = linked.NewHashmap[K, V]()
|
||||
}
|
||||
if c.Size <= 0 {
|
||||
c.Size = 1
|
||||
}
|
||||
for c.elements.Len() > c.Size {
|
||||
oldestKey, _, _ := c.elements.Oldest()
|
||||
c.elements.Delete(oldestKey)
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,32 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Package metercacher provides metered cache implementations.
|
||||
package metercacher
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/cache"
|
||||
"github.com/luxfi/metric"
|
||||
|
||||
"github.com/luxfi/cache"
|
||||
)
|
||||
|
||||
var _ cache.Cacher[struct{}, struct{}] = (*Cache[struct{}, struct{}])(nil)
|
||||
|
||||
// Cache wraps a Cacher with metrics.
|
||||
type Cache[K comparable, V any] struct {
|
||||
cache.Cacher[K, V]
|
||||
|
||||
metrics *cacheMetrics
|
||||
}
|
||||
|
||||
// New creates a new metered cache wrapper.
|
||||
func New[K comparable, V any](
|
||||
namespace string,
|
||||
registry metric.Registry,
|
||||
c cache.Cacher[K, V],
|
||||
cache cache.Cacher[K, V],
|
||||
) (*Cache[K, V], error) {
|
||||
metrics, err := newMetrics(namespace, registry)
|
||||
return &Cache[K, V]{
|
||||
Cacher: c,
|
||||
Cacher: cache,
|
||||
metrics: metrics,
|
||||
}, err
|
||||
}
|
||||
@@ -61,12 +60,14 @@ func (c *Cache[K, V]) Get(key K) (V, bool) {
|
||||
|
||||
func (c *Cache[K, _]) Evict(key K) {
|
||||
c.Cacher.Evict(key)
|
||||
|
||||
c.metrics.len.Set(float64(c.Cacher.Len()))
|
||||
c.metrics.portionFilled.Set(c.Cacher.PortionFilled())
|
||||
}
|
||||
|
||||
func (c *Cache[_, _]) Flush() {
|
||||
c.Cacher.Flush()
|
||||
|
||||
c.metrics.len.Set(float64(c.Cacher.Len()))
|
||||
c.metrics.portionFilled.Set(c.Cacher.PortionFilled())
|
||||
}
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
|
||||
package metercacher
|
||||
|
||||
import (
|
||||
"github.com/luxfi/metric"
|
||||
)
|
||||
import "github.com/luxfi/metric"
|
||||
|
||||
const (
|
||||
resultLabel = "result"
|
||||
|
||||
Reference in New Issue
Block a user