Initial release

This commit is contained in:
2025-12-04 08:38:33 -08:00
commit db55166d12
6 changed files with 341 additions and 0 deletions
+26
View File
@@ -0,0 +1,26 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package cache provides caching interfaces and implementations.
package cache
// Cacher acts as a best effort key value store.
type Cacher[K comparable, V any] interface {
// Put inserts an element into the cache.
Put(key K, value V)
// Get returns the entry with the key, if it exists.
Get(key K) (V, bool)
// Evict removes the specified entry from the cache.
Evict(key K)
// Flush removes all entries from the cache.
Flush()
// Len returns the number of elements in the cache.
Len() int
// PortionFilled returns fraction of cache currently filled (0 --> 1).
PortionFilled() float64
}
+17
View File
@@ -0,0 +1,17 @@
module github.com/luxfi/cache
go 1.25
require github.com/luxfi/metric v1.4.5
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_golang v1.23.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
golang.org/x/sys v0.35.0 // indirect
google.golang.org/protobuf v1.36.7 // indirect
)
+36
View File
@@ -0,0 +1,36 @@
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/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/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
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/metric v1.4.5 h1:fD6gtpzB5ebGB06/m4K7e5+tOV5RiVHSZCV+Zb8ZDxk=
github.com/luxfi/metric v1.4.5/go.mod h1:AUQ7NSNz9WndAcr/SKnOkP7XSFFnBXOa+ihtJYfDaQY=
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=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
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.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+118
View File
@@ -0,0 +1,118 @@
// 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 (
"container/list"
"sync"
"github.com/luxfi/cache"
)
var _ cache.Cacher[struct{}, struct{}] = (*Cache[struct{}, struct{}])(nil)
// entry is a cache entry.
type entry[K comparable, V any] struct {
key K
value V
}
// Cache is a thread-safe LRU cache.
type Cache[K comparable, V any] struct {
lock sync.Mutex
size int
elements map[K]*list.Element
order *list.List
}
// NewCache creates a new LRU cache with the specified size.
func NewCache[K comparable, V any](size int) *Cache[K, V] {
if size <= 0 {
size = 1
}
return &Cache[K, V]{
size: size,
elements: make(map[K]*list.Element),
order: list.New(),
}
}
// Put inserts an element into the cache.
func (c *Cache[K, V]) Put(key K, value V) {
c.lock.Lock()
defer c.lock.Unlock()
if elem, ok := c.elements[key]; ok {
// Update existing entry
elem.Value.(*entry[K, V]).value = value
c.order.MoveToFront(elem)
return
}
// Evict oldest if at capacity
if c.order.Len() >= c.size {
oldest := c.order.Back()
if oldest != nil {
c.removeElement(oldest)
}
}
// Add new entry
e := &entry[K, V]{key: key, value: value}
elem := c.order.PushFront(e)
c.elements[key] = elem
}
// Get returns the entry with the key, if it exists.
func (c *Cache[K, V]) Get(key K) (V, bool) {
c.lock.Lock()
defer c.lock.Unlock()
if elem, ok := c.elements[key]; ok {
c.order.MoveToFront(elem)
return elem.Value.(*entry[K, V]).value, true
}
var zero V
return zero, false
}
// Evict removes the specified entry from the cache.
func (c *Cache[K, V]) Evict(key K) {
c.lock.Lock()
defer c.lock.Unlock()
if elem, ok := c.elements[key]; ok {
c.removeElement(elem)
}
}
// Flush removes all entries from the cache.
func (c *Cache[K, V]) Flush() {
c.lock.Lock()
defer c.lock.Unlock()
c.elements = make(map[K]*list.Element)
c.order.Init()
}
// Len returns the number of elements in the cache.
func (c *Cache[K, V]) Len() int {
c.lock.Lock()
defer c.lock.Unlock()
return c.order.Len()
}
// PortionFilled returns fraction of cache currently filled.
func (c *Cache[K, V]) PortionFilled() float64 {
c.lock.Lock()
defer c.lock.Unlock()
return float64(c.order.Len()) / float64(c.size)
}
func (c *Cache[K, V]) removeElement(elem *list.Element) {
e := elem.Value.(*entry[K, V])
delete(c.elements, e.key)
c.order.Remove(elem)
}
+72
View File
@@ -0,0 +1,72 @@
// 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"
)
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[K, V], error) {
metrics, err := newMetrics(namespace, registry)
return &Cache[K, V]{
Cacher: c,
metrics: metrics,
}, err
}
func (c *Cache[K, V]) Put(key K, value V) {
start := time.Now()
c.Cacher.Put(key, value)
putDuration := time.Since(start)
c.metrics.putCount.Inc()
c.metrics.putTime.Add(float64(putDuration))
c.metrics.len.Set(float64(c.Cacher.Len()))
c.metrics.portionFilled.Set(c.Cacher.PortionFilled())
}
func (c *Cache[K, V]) Get(key K) (V, bool) {
start := time.Now()
value, has := c.Cacher.Get(key)
getDuration := time.Since(start)
if has {
c.metrics.getCount.With(hitLabels).Inc()
c.metrics.getTime.With(hitLabels).Add(float64(getDuration))
} else {
c.metrics.getCount.With(missLabels).Inc()
c.metrics.getTime.With(missLabels).Add(float64(getDuration))
}
return value, has
}
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())
}
+72
View File
@@ -0,0 +1,72 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package metercacher
import (
"github.com/luxfi/metric"
)
const (
resultLabel = "result"
hitResult = "hit"
missResult = "miss"
)
var (
resultLabels = []string{resultLabel}
hitLabels = metric.Labels{
resultLabel: hitResult,
}
missLabels = metric.Labels{
resultLabel: missResult,
}
)
type cacheMetrics struct {
getCount metric.CounterVec
getTime metric.GaugeVec
putCount metric.Counter
putTime metric.Gauge
len metric.Gauge
portionFilled metric.Gauge
}
func newMetrics(
namespace string,
registry metric.Registry,
) (*cacheMetrics, error) {
metricsInstance := metric.NewWithRegistry(namespace, registry)
m := &cacheMetrics{
getCount: metricsInstance.NewCounterVec(
"get_count",
"number of get calls",
resultLabels,
),
getTime: metricsInstance.NewGaugeVec(
"get_time",
"time spent (ns) in get calls",
resultLabels,
),
putCount: metricsInstance.NewCounter(
"put_count",
"number of put calls",
),
putTime: metricsInstance.NewGauge(
"put_time",
"time spent (ns) in put calls",
),
len: metricsInstance.NewGauge(
"len",
"number of entries",
),
portionFilled: metricsInstance.NewGauge(
"portion_filled",
"fraction of cache filled",
),
}
return m, nil
}