feat: add LRU and DualMapCache types, update to metric v1.4.10

This commit is contained in:
2026-01-09 00:38:55 -08:00
parent 6094bb5be9
commit 8c43f5b82c
11 changed files with 544 additions and 205 deletions
+73
View File
@@ -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() {}
+28
View File
@@ -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
}
+17 -3
View File
@@ -2,20 +2,34 @@ module github.com/luxfi/cache
go 1.25.5 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 ( require (
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // 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/klauspost/compress v1.18.2 // indirect
github.com/kr/text v0.2.0 // 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/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_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.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/prometheus/procfs v0.19.2 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
go.yaml.in/yaml/v2 v2.4.3 // 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 golang.org/x/sys v0.39.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )
+25 -2
View File
@@ -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/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 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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 h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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 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 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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 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 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 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 h1:fD6gtpzB5ebGB06/m4K7e5+tOV5RiVHSZCV+Zb8ZDxk=
github.com/luxfi/metric v1.4.5/go.mod h1:AUQ7NSNz9WndAcr/SKnOkP7XSFFnBXOa+ihtJYfDaQY= 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 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 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= 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_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 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 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.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= 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 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 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.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 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= 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 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
+117
View File
@@ -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)
+51
View File
@@ -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
View File
@@ -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
View File
@@ -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. // See the file LICENSE for licensing terms.
package lru package lru
import ( import (
"container/list"
"sync" "sync"
"github.com/luxfi/cache" "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 type sizedEntry[K comparable, V any] struct {
// calculate the size multiple times. key K
//
// This ensures that any inconsistencies returned by the size function can not
// corrupt the cache.
type sizedElement[V any] struct {
value V value V
size int size int
} }
// SizedCache is a key value store with bounded size. If the size is attempted // NewSizedCache creates a size-bounded LRU cache.
// to be exceeded, then elements are removed from the cache until the bound is func NewSizedCache[K comparable, V any](maxSize int, sizeFn func(K, V) int) *SizedCache[K, V] {
// honored, based on evicting the least recently used value. if maxSize <= 0 {
type SizedCache[K comparable, V any] struct { maxSize = 1
lock sync.Mutex }
elements *linked.Hashmap[K, *sizedElement[V]] if sizeFn == nil {
maxSize int sizeFn = func(K, V) int { return 1 }
currentSize int }
size func(K, V) int
}
func NewSizedCache[K comparable, V any](maxSize int, size func(K, V) int) *SizedCache[K, V] {
return &SizedCache[K, V]{ return &SizedCache[K, V]{
elements: linked.NewHashmap[K, *sizedElement[V]](), maxSize: maxSize,
maxSize: maxSize, sizeFn: sizeFn,
size: size, 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) { func (c *SizedCache[K, V]) Put(key K, value V) {
c.lock.Lock() c.mu.Lock()
defer c.lock.Unlock() defer c.mu.Unlock()
c.put(key, value) entrySize := c.sizeFn(key, value)
} if entrySize > c.maxSize {
c.flushLocked()
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()
return return
} }
if oldElement, ok := c.elements.Get(key); ok { if elem, ok := c.items[key]; ok {
c.currentSize -= oldElement.size 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-entrySize {
for c.currentSize > c.maxSize-newEntrySize { back := c.lru.Back()
oldestKey, oldestElement, _ := c.elements.Oldest() if back == nil {
c.elements.Delete(oldestKey) break
c.currentSize -= oldestElement.size }
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]{ e := &sizedEntry[K, V]{key: key, value: value, size: entrySize}
value: value, c.items[key] = c.lru.PushFront(e)
size: newEntrySize, c.currentSize += entrySize
})
c.currentSize += newEntrySize
} }
func (c *SizedCache[K, V]) get(key K) (V, bool) { // Get retrieves a value and marks it as most recently used.
element, ok := c.elements.Get(key) func (c *SizedCache[K, V]) Get(key K) (V, bool) {
if !ok { c.mu.Lock()
return utils.Zero[V](), false defer c.mu.Unlock()
}
c.elements.Put(key, element) // Mark [k] as MRU. if elem, ok := c.items[key]; ok {
return element.value, true 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) { // Evict removes a key from the cache.
if element, ok := c.elements.Get(key); ok { func (c *SizedCache[K, V]) Evict(key K) {
c.elements.Delete(key) c.mu.Lock()
c.currentSize -= element.size 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() { // Flush removes all entries.
c.elements.Clear() 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 c.currentSize = 0
} }
func (c *SizedCache[_, _]) len() int { // Len returns number of entries.
return c.elements.Len() 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) return float64(c.currentSize) / float64(c.maxSize)
} }
var _ cache.Cacher[struct{}, struct{}] = (*SizedCache[struct{}, struct{}])(nil)
+133
View File
@@ -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)
}
}
+7 -6
View File
@@ -1,33 +1,32 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved. // Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms. // See the file LICENSE for licensing terms.
// Package metercacher provides metered cache implementations.
package metercacher package metercacher
import ( import (
"time" "time"
"github.com/luxfi/cache"
"github.com/luxfi/metric" "github.com/luxfi/metric"
"github.com/luxfi/cache"
) )
var _ cache.Cacher[struct{}, struct{}] = (*Cache[struct{}, struct{}])(nil) var _ cache.Cacher[struct{}, struct{}] = (*Cache[struct{}, struct{}])(nil)
// Cache wraps a Cacher with metrics.
type Cache[K comparable, V any] struct { type Cache[K comparable, V any] struct {
cache.Cacher[K, V] cache.Cacher[K, V]
metrics *cacheMetrics metrics *cacheMetrics
} }
// New creates a new metered cache wrapper.
func New[K comparable, V any]( func New[K comparable, V any](
namespace string, namespace string,
registry metric.Registry, registry metric.Registry,
c cache.Cacher[K, V], cache cache.Cacher[K, V],
) (*Cache[K, V], error) { ) (*Cache[K, V], error) {
metrics, err := newMetrics(namespace, registry) metrics, err := newMetrics(namespace, registry)
return &Cache[K, V]{ return &Cache[K, V]{
Cacher: c, Cacher: cache,
metrics: metrics, metrics: metrics,
}, err }, err
} }
@@ -61,12 +60,14 @@ func (c *Cache[K, V]) Get(key K) (V, bool) {
func (c *Cache[K, _]) Evict(key K) { func (c *Cache[K, _]) Evict(key K) {
c.Cacher.Evict(key) c.Cacher.Evict(key)
c.metrics.len.Set(float64(c.Cacher.Len())) c.metrics.len.Set(float64(c.Cacher.Len()))
c.metrics.portionFilled.Set(c.Cacher.PortionFilled()) c.metrics.portionFilled.Set(c.Cacher.PortionFilled())
} }
func (c *Cache[_, _]) Flush() { func (c *Cache[_, _]) Flush() {
c.Cacher.Flush() c.Cacher.Flush()
c.metrics.len.Set(float64(c.Cacher.Len())) c.metrics.len.Set(float64(c.Cacher.Len()))
c.metrics.portionFilled.Set(c.Cacher.PortionFilled()) c.metrics.portionFilled.Set(c.Cacher.PortionFilled())
} }
+1 -3
View File
@@ -3,9 +3,7 @@
package metercacher package metercacher
import ( import "github.com/luxfi/metric"
"github.com/luxfi/metric"
)
const ( const (
resultLabel = "result" resultLabel = "result"