Skip to content

Commit 1be5936

Browse files
authored
Merge pull request #43 from hyp3rd/feat/eviction-improvements
eviction: fix LFU tie-breaking with LRU-on-ties using monotonic seq; …
2 parents 82bed8d + e7320c4 commit 1be5936

File tree

5 files changed

+27
-11
lines changed

5 files changed

+27
-11
lines changed

README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ It ships with a default [historigram stats collector](./pkg/stats/stats.go) and
1111
- [Recently Used (LRU) eviction algorithm](./pkg/eviction/lru.go)
1212
- [The Least Frequently Used (LFU) algorithm](./pkg/eviction/lfu.go)
1313
- [Cache-Aware Write-Optimized LFU (CAWOLFU)](./pkg/eviction/cawolfu.go)
14-
- [The Adaptive Replacement Cache (ARC) algorithm](./pkg/eviction/arc.go)
14+
- [The Adaptive Replacement Cache (ARC) algorithm](./pkg/eviction/arc.go) — Experimental (not enabled by default)
1515
- [The clock eviction algorithm](./pkg/eviction/clock.go)
1616

1717
### Features
@@ -25,7 +25,7 @@ It ships with a default [historigram stats collector](./pkg/stats/stats.go) and
2525
- Retrieve items from the cache by their key
2626
- Delete items from the cache by their key
2727
- Clear the cache of all items
28-
- Evitc items in the background based on the cache capacity and items access leveraging several custom eviction algorithms
28+
- Evict items in the background based on the cache capacity and items access leveraging several custom eviction algorithms
2929
- Expire items in the background based on their duration
3030
- [Eviction Algorithm interface](./pkg/eviction/eviction.go) to implement custom eviction algorithms.
3131
- 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,
8787

8888
Use your preferred OpenTelemetry SDK setup for exporters and processors in production; the example uses no-op providers for simplicity.
8989

90+
### Eviction algorithms
91+
92+
Available algorithm names you can pass to `WithEvictionAlgorithm`:
93+
94+
- "lru" — Least Recently Used (default)
95+
- "lfu" — Least Frequently Used (with LRU tie-breaker for equal frequencies)
96+
- "clock" — Second-chance clock
97+
- "cawolfu" — Cache-Aware Write-Optimized LFU
98+
- "arc" — Adaptive Replacement Cache (experimental; not registered by default)
99+
100+
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.
101+
90102
## API
91103

92104
The `NewInMemoryWithDefaults` function creates a new `HyperCache` instance with the defaults:

config.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Package hypercache provides a high-performance, generic caching library with configurable backends and eviction algorithms.
2-
// It supports multiple backend types including in-memory and Redis, with various eviction strategies like LRU, LFU, ARC, and more.
2+
// It supports multiple backend types including in-memory and Redis, with various eviction strategies like LRU, LFU, and more.
33
// The package is designed to be flexible and extensible, allowing users to customize cache behavior through configuration options.
44
//
55
// Example usage:
@@ -90,7 +90,7 @@ func WithMaxCacheSize[T backend.IBackendConstrain](maxCacheSize int64) Option[T]
9090
// - "FIFO" (First In First Out)
9191
// - "RANDOM" (Random)
9292
// - "CLOCK" (Clock) - Implemented in the `eviction/clock.go` file
93-
// - "ARC" (Adaptive Replacement Cache) - Implemented in the `eviction/arc.go` file
93+
// - "ARC" (Adaptive Replacement Cache) - Experimental (not enabled by default)
9494
// - "TTL" (Time To Live)
9595
// - "LFUDA" (Least Frequently Used with Dynamic Aging)
9696
// - "SLRU" (Segmented Least Recently Used)

pkg/eviction/eviction.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,6 @@ type AlgorithmRegistry struct {
2828
// getDefaultAlgorithms returns the default set of eviction algorithms.
2929
func getDefaultAlgorithms() map[string]func(capacity int) (IAlgorithm, error) {
3030
return map[string]func(capacity int) (IAlgorithm, error){
31-
"arc": func(capacity int) (IAlgorithm, error) {
32-
return NewARCAlgorithm(capacity)
33-
},
3431
"lru": func(capacity int) (IAlgorithm, error) {
3532
return NewLRUAlgorithm(capacity)
3633
},

pkg/eviction/lfu.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type LFUAlgorithm struct {
1515
mutex sync.RWMutex
1616
length int
1717
cap int
18+
seq uint64 // monotonic sequence to break frequency ties by recency (LRU on ties)
1819
}
1920

2021
// Node is a node in the LFUAlgorithm.
@@ -23,6 +24,7 @@ type Node struct {
2324
value any
2425
count int
2526
index int
27+
last uint64 // last access sequence (higher = more recent)
2628
}
2729

2830
// FrequencyHeap is a heap of Nodes.
@@ -36,7 +38,8 @@ func (fh FrequencyHeap) Len() int { return len(fh) }
3638
// Less returns true if the node at index i has a lower frequency than the node at index j.
3739
func (fh FrequencyHeap) Less(i, j int) bool {
3840
if fh[i].count == fh[j].count {
39-
return fh[i].index < fh[j].index
41+
// On ties, evict the least recently used (older last sequence has priority)
42+
return fh[i].last < fh[j].last
4043
}
4144

4245
return fh[i].count < fh[j].count
@@ -82,6 +85,7 @@ func NewLFUAlgorithm(capacity int) (*LFUAlgorithm, error) {
8285
freqs: &FrequencyHeap{},
8386
length: 0,
8487
cap: capacity,
88+
seq: 0,
8589
}, nil
8690
}
8791

@@ -111,6 +115,8 @@ func (l *LFUAlgorithm) Set(key string, value any) {
111115
// Key exists: update value and increment frequency
112116
node.value = value
113117
node.count++
118+
l.seq++
119+
node.last = l.seq
114120
heap.Fix(l.freqs, node.index)
115121

116122
return
@@ -120,10 +126,12 @@ func (l *LFUAlgorithm) Set(key string, value any) {
120126
_, _ = l.internalEvict()
121127
}
122128

129+
l.seq++
123130
node := &Node{
124131
key: key,
125132
value: value,
126133
count: 1,
134+
last: l.seq,
127135
}
128136
l.items[key] = node
129137
heap.Push(l.freqs, node)
@@ -141,6 +149,8 @@ func (l *LFUAlgorithm) Get(key string) (any, bool) {
141149
}
142150

143151
node.count++
152+
l.seq++
153+
node.last = l.seq
144154
heap.Fix(l.freqs, node.index)
145155

146156
return node.value, true

pkg/middleware/otel_metrics.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,6 @@ func (mw *OTelMetricsMiddleware) Stop() { mw.next.Stop() }
136136
func (mw *OTelMetricsMiddleware) GetStats() stats.Stats { return mw.next.GetStats() }
137137

138138
// rec records call count and duration with attributes.
139-
// Moved to the end to satisfy funcorder linters.
140139
func (mw *OTelMetricsMiddleware) rec(ctx context.Context, method string, start time.Time, attrs ...attribute.KeyValue) {
141140
base := []attribute.KeyValue{attribute.String("method", method)}
142141
if len(attrs) > 0 {
@@ -146,5 +145,3 @@ func (mw *OTelMetricsMiddleware) rec(ctx context.Context, method string, start t
146145
mw.calls.Add(ctx, 1, metric.WithAttributes(base...))
147146
mw.durations.Record(ctx, float64(time.Since(start).Milliseconds()), metric.WithAttributes(base...))
148147
}
149-
150-
// keep helpers at end of file

0 commit comments

Comments
 (0)