diff --git a/README.md b/README.md index a1f2545..db910ef 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ It ships with a default [historigram stats collector](./pkg/stats/stats.go) and - [Recently Used (LRU) eviction algorithm](./pkg/eviction/lru.go) - [The Least Frequently Used (LFU) algorithm](./pkg/eviction/lfu.go) - [Cache-Aware Write-Optimized LFU (CAWOLFU)](./pkg/eviction/cawolfu.go) -- [The Adaptive Replacement Cache (ARC) algorithm](./pkg/eviction/arc.go) +- [The Adaptive Replacement Cache (ARC) algorithm](./pkg/eviction/arc.go) — Experimental (not enabled by default) - [The clock eviction algorithm](./pkg/eviction/clock.go) ### Features @@ -25,7 +25,7 @@ It ships with a default [historigram stats collector](./pkg/stats/stats.go) and - Retrieve items from the cache by their key - Delete items from the cache by their key - Clear the cache of all items -- Evitc items in the background based on the cache capacity and items access leveraging several custom eviction algorithms +- Evict items in the background based on the cache capacity and items access leveraging several custom eviction algorithms - Expire items in the background based on their duration - [Eviction Algorithm interface](./pkg/eviction/eviction.go) to implement custom eviction algorithms. - Stats collection with a default [stats collector](./pkg/stats/stats.go) or a custom one that implements the StatsCollector interface. @@ -87,6 +87,18 @@ svc := hypercache.ApplyMiddleware(svc, Use your preferred OpenTelemetry SDK setup for exporters and processors in production; the example uses no-op providers for simplicity. +### Eviction algorithms + +Available algorithm names you can pass to `WithEvictionAlgorithm`: + +- "lru" — Least Recently Used (default) +- "lfu" — Least Frequently Used (with LRU tie-breaker for equal frequencies) +- "clock" — Second-chance clock +- "cawolfu" — Cache-Aware Write-Optimized LFU +- "arc" — Adaptive Replacement Cache (experimental; not registered by default) + +Note: ARC is experimental and isn’t included in the default registry. If you choose to use it, register it manually or enable it explicitly in your build. + ## API The `NewInMemoryWithDefaults` function creates a new `HyperCache` instance with the defaults: diff --git a/config.go b/config.go index e940322..410ac76 100644 --- a/config.go +++ b/config.go @@ -1,5 +1,5 @@ // Package hypercache provides a high-performance, generic caching library with configurable backends and eviction algorithms. -// It supports multiple backend types including in-memory and Redis, with various eviction strategies like LRU, LFU, ARC, and more. +// It supports multiple backend types including in-memory and Redis, with various eviction strategies like LRU, LFU, and more. // The package is designed to be flexible and extensible, allowing users to customize cache behavior through configuration options. // // Example usage: @@ -90,7 +90,7 @@ func WithMaxCacheSize[T backend.IBackendConstrain](maxCacheSize int64) Option[T] // - "FIFO" (First In First Out) // - "RANDOM" (Random) // - "CLOCK" (Clock) - Implemented in the `eviction/clock.go` file -// - "ARC" (Adaptive Replacement Cache) - Implemented in the `eviction/arc.go` file +// - "ARC" (Adaptive Replacement Cache) - Experimental (not enabled by default) // - "TTL" (Time To Live) // - "LFUDA" (Least Frequently Used with Dynamic Aging) // - "SLRU" (Segmented Least Recently Used) diff --git a/pkg/eviction/eviction.go b/pkg/eviction/eviction.go index 4c695c9..bdcaa4d 100644 --- a/pkg/eviction/eviction.go +++ b/pkg/eviction/eviction.go @@ -28,9 +28,6 @@ type AlgorithmRegistry struct { // getDefaultAlgorithms returns the default set of eviction algorithms. func getDefaultAlgorithms() map[string]func(capacity int) (IAlgorithm, error) { return map[string]func(capacity int) (IAlgorithm, error){ - "arc": func(capacity int) (IAlgorithm, error) { - return NewARCAlgorithm(capacity) - }, "lru": func(capacity int) (IAlgorithm, error) { return NewLRUAlgorithm(capacity) }, diff --git a/pkg/eviction/lfu.go b/pkg/eviction/lfu.go index 1f23da1..059c5cf 100644 --- a/pkg/eviction/lfu.go +++ b/pkg/eviction/lfu.go @@ -15,6 +15,7 @@ type LFUAlgorithm struct { mutex sync.RWMutex length int cap int + seq uint64 // monotonic sequence to break frequency ties by recency (LRU on ties) } // Node is a node in the LFUAlgorithm. @@ -23,6 +24,7 @@ type Node struct { value any count int index int + last uint64 // last access sequence (higher = more recent) } // FrequencyHeap is a heap of Nodes. @@ -36,7 +38,8 @@ func (fh FrequencyHeap) Len() int { return len(fh) } // Less returns true if the node at index i has a lower frequency than the node at index j. func (fh FrequencyHeap) Less(i, j int) bool { if fh[i].count == fh[j].count { - return fh[i].index < fh[j].index + // On ties, evict the least recently used (older last sequence has priority) + return fh[i].last < fh[j].last } return fh[i].count < fh[j].count @@ -82,6 +85,7 @@ func NewLFUAlgorithm(capacity int) (*LFUAlgorithm, error) { freqs: &FrequencyHeap{}, length: 0, cap: capacity, + seq: 0, }, nil } @@ -111,6 +115,8 @@ func (l *LFUAlgorithm) Set(key string, value any) { // Key exists: update value and increment frequency node.value = value node.count++ + l.seq++ + node.last = l.seq heap.Fix(l.freqs, node.index) return @@ -120,10 +126,12 @@ func (l *LFUAlgorithm) Set(key string, value any) { _, _ = l.internalEvict() } + l.seq++ node := &Node{ key: key, value: value, count: 1, + last: l.seq, } l.items[key] = node heap.Push(l.freqs, node) @@ -141,6 +149,8 @@ func (l *LFUAlgorithm) Get(key string) (any, bool) { } node.count++ + l.seq++ + node.last = l.seq heap.Fix(l.freqs, node.index) return node.value, true diff --git a/pkg/middleware/otel_metrics.go b/pkg/middleware/otel_metrics.go index 8469fcc..db4c4a6 100644 --- a/pkg/middleware/otel_metrics.go +++ b/pkg/middleware/otel_metrics.go @@ -136,7 +136,6 @@ func (mw *OTelMetricsMiddleware) Stop() { mw.next.Stop() } func (mw *OTelMetricsMiddleware) GetStats() stats.Stats { return mw.next.GetStats() } // rec records call count and duration with attributes. -// Moved to the end to satisfy funcorder linters. func (mw *OTelMetricsMiddleware) rec(ctx context.Context, method string, start time.Time, attrs ...attribute.KeyValue) { base := []attribute.KeyValue{attribute.String("method", method)} if len(attrs) > 0 { @@ -146,5 +145,3 @@ func (mw *OTelMetricsMiddleware) rec(ctx context.Context, method string, start t mw.calls.Add(ctx, 1, metric.WithAttributes(base...)) mw.durations.Record(ctx, float64(time.Since(start).Milliseconds()), metric.WithAttributes(base...)) } - -// keep helpers at end of file