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
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,49 @@

All notable changes to this project will be documented in this file.

## v1.3.0

This release simplifies cache construction, makes background cleanup lifecycle explicit, and streamlines metrics
integration.

### Added

* **Cache Naming:** Added `WithName` for assigning an optional cache name used by observability integrations.
* **Cleanup Lifecycle:** Added `StartCleanup` and `StopCleanup` for explicitly controlling periodic expiration cleanup.
* **Metrics Sources:** Added `MetricsSource` for exposing cache statistics to metrics integrations.

### Changed

* **Constructors:** `New` no longer accepts a cache name. `NewWithDefaultLoader` now accepts the loader directly,
followed by options.
* **Metrics API:** Replaced `StatsProvider` and `MetricsRegistration` with `Metrics.Register(MetricsSource) error`.
* **Background Cleanup:** Cleanup is no longer started automatically during cache construction. `WithCleanupInterval`
configures the cleanup interval, while `StartCleanup` runs the blocking cleanup loop explicitly.
* **Cleanup Defaults:** Background cleanup uses a one-minute interval by default.
* **Benchmarks:** Updated benchmark suites and comparisons for the current cache API.

### Removed

* **Cache Close:** Removed `Cache.Close`; background cleanup is stopped explicitly with `StopCleanup`.

## extra/paceotel/v1.3.0

This release aligns `paceotel` with the updated `pacecache` metrics API and simplifies metrics registration across
multiple caches.

### Added

* **Reusable Metrics:** A single `Metrics` instance can be shared across multiple caches.
* **Source Validation:** Duplicate cache names are rejected within a `Metrics` instance, while a single unnamed cache is
supported.

### Changed

* **Metrics Registration:** Each cache now uses its own OpenTelemetry callback registration.
* **Unregister Lifecycle:** `Metrics.Unregister` removes all registered callbacks, releases references to registered
caches, and prevents further registrations.
* **Core Dependency:** Updated `github.com/mkbeh/pacecache` to v1.3.0.

## v1.2.1

This release streamlines cache removal and makes cache-aside loading configurable through default and per-call loaders.
Expand Down
29 changes: 14 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ The library provides an intuitive API with predictable behavior under high concu
* **Generic API:** Type-safe caching with comparable keys and arbitrary value types.
* **Bounded LRU:** Exact per-segment LRU within a fixed total capacity.
* **Expiration:** Default and per-entry TTLs, jitter, sliding expiration, refresh, and no-expiration entries.
* **Cleanup:** Lazy expiration, explicit cleanup, and an optional background worker.
* **Cleanup:** Lazy expiration, explicit cleanup, and optional background cleanup.
* **Cache-Aside:** Coalesces concurrent misses for the same key into a single load.
* **Safe Updates:** Publication barriers prevent stale loads from overwriting newer cache state.
* **Observability:** Built-in statistics with optional OpenTelemetry metrics.
Expand All @@ -42,15 +42,14 @@ go get github.com/mkbeh/pacecache/extra/paceotel

## Usage

Create a cache with `pacecache.New` and close it when it is no longer needed:
Create a cache with `pacecache.New`:

<!-- @formatter:off -->
```go
cache, err := pacecache.New[string, string]("cache")
cache, err := pacecache.New[string, string]()
if err != nil {
panic(err)
}
defer cache.Close()
```
<!-- @formatter:on -->

Expand All @@ -64,11 +63,9 @@ deadlines and reduce synchronized expiration bursts. Individual entries can use
<!-- @formatter:off -->
```go
cache, _ := pacecache.New[string, string](
"cache",
pacecache.WithTTL(5*time.Minute),
pacecache.WithJitter(30*time.Second),
)
defer cache.Close()
```
<!-- @formatter:on -->

Expand All @@ -86,6 +83,7 @@ value, found := cache.Get("key1")

// Read a value together with its expiration metadata.
entry, found := cache.GetEntry("key1")
fmt.Println(entry.Value(), entry.ExpiresAt())

// Check existence without updating LRU or TTL.
exists := cache.Exists("key2")
Expand Down Expand Up @@ -131,14 +129,12 @@ If the same loader is reused across calls, configure it once with `NewWithDefaul
<!-- @formatter:off -->
```go
cache, _ := pacecache.NewWithDefaultLoader[string, string](
"cache",
func(ctx context.Context, key string) (string, bool, error) {
// Fetch data from a database, file, or remote service.
return "loaded value", true, nil
},
pacecache.WithTTL(5*time.Minute),
)
defer cache.Close()

// Return the cached value or invoke the configured loader on a miss.
value, found, err := cache.GetOrLoad(ctx, "key")
Expand All @@ -155,22 +151,25 @@ if found {
Missing results and loader errors are returned without being cached. Concurrent misses for the same key share a single
loader execution, avoiding duplicate requests to the upstream source.

Expired entries are never returned and are removed lazily when encountered. Periodic background cleanup can be enabled
for entries that may remain untouched:
Expired entries are removed lazily when encountered. Background cleanup can be started with `StartCleanup`. Since
`StartCleanup` blocks until `StopCleanup` is called, it is usually launched in a separate goroutine:

<!-- @formatter:off -->
```go
cache, _ := pacecache.New[string, string](
"cache",
pacecache.WithTTL(5*time.Minute),
pacecache.WithCleanupInterval(time.Minute), // background cleanup
)
defer cache.Close()

// Start automatic deletion of expired items.
go cache.StartCleanup()

// Stop automatic deletion of expired items.
cache.StopCleanup()
```
<!-- @formatter:on -->

Background cleanup is optional. Expired entries can also be reclaimed explicitly with `DeleteExpired`. `Close` stops
the cleanup worker and waits for it to exit.
Background cleanup is optional. Expired entries can also be reclaimed explicitly with `DeleteExpired`.

## Concurrency semantics

The cache coordinates concurrent loads and mutations to prevent duplicate upstream work and stale values from
Expand Down
Binary file modified benchmarks/performance/hitratio/assets/hit-ratio.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 0 additions & 5 deletions benchmarks/performance/hitratio/internal/policy/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ type Policy struct {

func New(capacity int, segments int) (*Policy, error) {
cache, err := pacecache.New[uint64, uint64](
"hit-ratio",
pacecache.WithMaxEntries(capacity),
pacecache.WithSegmentCount(segments),
)
Expand Down Expand Up @@ -60,7 +59,3 @@ func (p *Policy) Ratio() float64 {

return 100 * float64(p.hits) / float64(total)
}

func (p *Policy) Close() {
p.cache.Close()
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ func (s Simulator) simulateCapacity(capacity int) (result, error) {
if err != nil {
return result{}, fmt.Errorf("create policy for capacity %d: %w", capacity, err)
}
defer p.Close()

generator := trace.NewZipf(
s.cfg.Zipf.S,
Expand Down
Binary file modified benchmarks/performance/memory/assets/memory.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 0 additions & 2 deletions benchmarks/performance/memory/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,13 @@ func main() {
runtime.ReadMemStats(&before)

cache, err := pacecache.New[string, string](
"memory",
pacecache.WithMaxEntries(*capacity),
pacecache.WithSegmentCount(segmentCount),
pacecache.WithTTL(expiration),
)
if err != nil {
log.Fatalf("create cache: %v", err)
}
defer cache.Close()

for index := range *capacity {
key := keys[index]
Expand Down
Binary file modified benchmarks/performance/throughput/assets/throughput.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 0 additions & 3 deletions benchmarks/performance/throughput/throughput_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ func runThroughputBenchmark(
b.Helper()

cache := newThroughputCache(b, maxEntries, data)
b.Cleanup(cache.Close)

var workers atomic.Uint64

Expand Down Expand Up @@ -133,7 +132,6 @@ func newThroughputCache(

for range throughputPopulationAttempts {
cache, err := pacecache.New[string, string](
"throughput",
pacecache.WithMaxEntries(maxEntries),
pacecache.WithSegmentCount(throughputSegments),
)
Expand All @@ -153,7 +151,6 @@ func newThroughputCache(
return cache
}

cache.Close()
}

b.Fatalf(
Expand Down
Loading
Loading