Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# HyperCache

Check failure on line 1 in README.md

View check run for this annotation

Trunk.io / Trunk Check

prettier

Incorrect formatting, autoformat by running 'trunk fmt'

[![Go](https://github.com/hyp3rd/hypercache/actions/workflows/go.yml/badge.svg)][build-link] [![CodeQL](https://github.com/hyp3rd/hypercache/actions/workflows/codeql.yml/badge.svg)][codeql-link]

Expand All @@ -11,7 +11,7 @@
- [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
Expand All @@ -25,7 +25,7 @@
- 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.
Expand Down Expand Up @@ -87,6 +87,18 @@

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:
Expand Down
4 changes: 2 additions & 2 deletions config.go
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 0 additions & 3 deletions pkg/eviction/eviction.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
},
Expand Down
12 changes: 11 additions & 1 deletion pkg/eviction/lfu.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -82,6 +85,7 @@ func NewLFUAlgorithm(capacity int) (*LFUAlgorithm, error) {
freqs: &FrequencyHeap{},
length: 0,
cap: capacity,
seq: 0,
Copy link

Copilot AI Aug 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initializing seq to 0 creates a potential issue where the first access will have sequence 1, but tie-breaking logic expects higher values to be more recent. Consider starting from 1 or documenting that 0 represents uninitialized state.

Suggested change
seq: 0,
seq: 1,

Copilot uses AI. Check for mistakes.
}, nil
}

Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand Down
3 changes: 0 additions & 3 deletions pkg/middleware/otel_metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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