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

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

## v1.4.0

This release simplifies cache construction and decouples the core cache API from optional metrics integrations.

### Added

* **Cache Name:** Added `Cache.Name` for retrieving the optional name configured through `WithName`.

### Changed

* **Constructors:** `New` and `NewWithLoader` now return a cache directly without an error. `NewWithDefaultLoader` was
renamed to `NewWithLoader`.
* **Options:** Options are now infallible. Invalid values leave the current setting unchanged, while segment counts
exceeding the entry budget are capped at the configured maximum.
* **Loader Configuration:** `NewWithLoader` now accepts a nil loader. `GetOrLoad` and `GetOrLoadEntry` return
`ErrNoLoader` on a cache miss when no loader is configured.
* **Metrics Integration:** Metrics registration is now handled explicitly by integrations through `Cache.Name` and
`Cache.Stats`.

### Removed

* **Core Metrics API:** Removed `WithMetrics`, `Metrics`, and `MetricsSource` from the core package.

## extra/paceotel/v1.4.0

This release makes OpenTelemetry registration explicit and independent from the core cache lifecycle.

### Added

* **Source Interface:** Added `Source` for exposing the cache name and statistics required for metrics collection.
* **Registration Attributes:** `Metrics.Register` now accepts additional OpenTelemetry attributes for each source.

### Changed

* **Metrics Registration:** Cache sources are now registered explicitly through `Metrics.Register` instead of
`pacecache.WithMetrics`.
* **Core Dependency:** Updated `github.com/mkbeh/pacecache` to v1.4.0.

## v1.3.0

This release simplifies cache construction, makes background cleanup lifecycle explicit, and streamlines metrics
Expand Down
35 changes: 10 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,7 @@ Create a cache with `pacecache.New`:

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

Expand All @@ -62,7 +59,7 @@ deadlines and reduce synchronized expiration bursts. Individual entries can use

<!-- @formatter:off -->
```go
cache, _ := pacecache.New[string, string](
cache := pacecache.New[string, string](
pacecache.WithTTL(5*time.Minute),
pacecache.WithJitter(30*time.Second),
)
Expand Down Expand Up @@ -114,21 +111,15 @@ loader := pacecache.Loader[string, string](func(ctx context.Context, key string)

// Return the cached value or invoke the loader on a miss.
value, found, err := cache.GetOrLoadFunc(ctx, "key", loader)
if err != nil {
panic(err)
}

if found {
fmt.Println("retrieved value:", value) // loaded key
}
fmt.Println("retrieved value:", value) // loaded value
```
<!-- @formatter:on -->

If the same loader is reused across calls, configure it once with `NewWithDefaultLoader` and use `GetOrLoad`:
If the same loader is reused across calls, configure it once with `NewWithLoader` and use `GetOrLoad`:

<!-- @formatter:off -->
```go
cache, _ := pacecache.NewWithDefaultLoader[string, string](
cache := pacecache.NewWithLoader[string, string](
func(ctx context.Context, key string) (string, bool, error) {
// Fetch data from a database, file, or remote service.
return "loaded value", true, nil
Expand All @@ -138,13 +129,7 @@ cache, _ := pacecache.NewWithDefaultLoader[string, string](

// Return the cached value or invoke the configured loader on a miss.
value, found, err := cache.GetOrLoad(ctx, "key")
if err != nil {
panic(err)
}

if found {
fmt.Println("retrieved value:", value) // loaded key
}
fmt.Println("retrieved value:", value) // loaded value
```
<!-- @formatter:on -->

Expand All @@ -156,7 +141,7 @@ Expired entries are removed lazily when encountered. Background cleanup can be s

<!-- @formatter:off -->
```go
cache, _ := pacecache.New[string, string](
cache := pacecache.New[string, string](
pacecache.WithTTL(5*time.Minute),
)

Expand Down Expand Up @@ -225,9 +210,9 @@ _ = stats.ClearedEntryCount // Entries removed by full cache clears
Statistics also include load outcomes, shared and superseded loads, deleted and cleared entry counts, cleanup activity,
and segment count.

Optional OpenTelemetry metrics are available through [paceotel](./extra/paceotel). OpenTelemetry configuration and
exporter selection remain application concerns, so Prometheus, OTLP, and other exporters can be used without changing
the cache integration.
Optional OpenTelemetry metrics are available through [paceotel](./extra/paceotel) and are registered explicitly for
each cache. OpenTelemetry configuration and exporter selection remain application concerns, so Prometheus, OTLP, and
other exporters can be used without changing the cache integration.

For a complete setup, see the [example](./examples/otel).

Expand Down
4 changes: 1 addition & 3 deletions benchmarks/performance/hitratio/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ func run(configPath string) error {

app := simulator.New(cfg)

if err := app.Simulate(os.Stdout); err != nil {
return fmt.Errorf("simulate trace: %w", err)
}
app.Simulate(os.Stdout)

return nil
}
23 changes: 7 additions & 16 deletions benchmarks/performance/hitratio/internal/policy/policy.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
package policy

import (
"fmt"

"github.com/mkbeh/pacecache"
)
import "github.com/mkbeh/pacecache"

type Policy struct {
cache *pacecache.Cache[uint64, uint64]
Expand All @@ -13,18 +9,13 @@ type Policy struct {
misses uint64
}

func New(capacity int, segments int) (*Policy, error) {
cache, err := pacecache.New[uint64, uint64](
pacecache.WithMaxEntries(capacity),
pacecache.WithSegmentCount(segments),
)
if err != nil {
return nil, fmt.Errorf("create cache: %w", err)
}

func New(capacity int, segments int) *Policy {
return &Policy{
cache: cache,
}, nil
cache: pacecache.New[uint64, uint64](
pacecache.WithMaxEntries(capacity),
pacecache.WithSegmentCount(segments),
),
}
}

func (p *Policy) Record(key uint64) {
Expand Down
17 changes: 5 additions & 12 deletions benchmarks/performance/hitratio/internal/simulator/simulator.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func New(cfg config.Config) Simulator {
}
}

func (s Simulator) Simulate(output io.Writer) error {
func (s Simulator) Simulate(output io.Writer) {
fmt.Fprintln(
output,
"trace,capacity,segments,requests,hits,misses,hit_ratio",
Expand All @@ -28,10 +28,7 @@ func (s Simulator) Simulate(output io.Writer) error {
for _, unsignedCapacity := range s.cfg.Capacities {
capacity := int(unsignedCapacity)

result, err := s.simulateCapacity(capacity)
if err != nil {
return err
}
result := s.simulateCapacity(capacity)

fmt.Fprintf(
output,
Expand All @@ -46,7 +43,6 @@ func (s Simulator) Simulate(output io.Writer) error {
)
}

return nil
}

type result struct {
Expand All @@ -56,11 +52,8 @@ type result struct {
Ratio float64
}

func (s Simulator) simulateCapacity(capacity int) (result, error) {
p, err := policy.New(capacity, s.cfg.Segments)
if err != nil {
return result{}, fmt.Errorf("create policy for capacity %d: %w", capacity, err)
}
func (s Simulator) simulateCapacity(capacity int) result {
p := policy.New(capacity, s.cfg.Segments)

generator := trace.NewZipf(
s.cfg.Zipf.S,
Expand All @@ -86,5 +79,5 @@ func (s Simulator) simulateCapacity(capacity int) (result, error) {
Hits: hits,
Misses: misses,
Ratio: p.Ratio(),
}, nil
}
}
6 changes: 1 addition & 5 deletions benchmarks/performance/memory/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,11 @@ func main() {
var before runtime.MemStats
runtime.ReadMemStats(&before)

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

for index := range *capacity {
key := keys[index]

Expand Down
6 changes: 1 addition & 5 deletions benchmarks/performance/throughput/throughput_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,10 @@ func newThroughputCache(
b.Helper()

for range throughputPopulationAttempts {
cache, err := pacecache.New[string, string](
cache := pacecache.New[string, string](
pacecache.WithMaxEntries(maxEntries),
pacecache.WithSegmentCount(throughputSegments),
)
if err != nil {
b.Fatalf("create cache: %v", err)
}

for index, key := range data.keys {
cache.Set(
key,
Expand Down
60 changes: 24 additions & 36 deletions cache.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package pacecache

import (
"fmt"
"math"
"math/rand/v2"
"sync"
"time"
Expand All @@ -15,6 +15,8 @@ const (
NoExpiration time.Duration = -1
)

const maxDuration = time.Duration(math.MaxInt64)

// Cache is a bounded in-process cache for keys of type K and values of type V.
//
// Cache uses exact LRU eviction within each storage segment and TTL expiration.
Expand All @@ -25,6 +27,7 @@ const (
// Cache is safe for concurrent use. A Cache must not be copied after creation.
type Cache[K comparable, V any] struct {
loader Loader[K, V]
name string

store *storage[K, V]
states []cacheState[K, V]
Expand All @@ -44,49 +47,42 @@ type Cache[K comparable, V any] struct {
//
// Unless overridden by options, New uses the default cache capacity, a single
// storage segment, and no time-based expiration. No default loader is
// configured. Metrics are disabled by default, and background cleanup is not
// started automatically.
// configured. Background cleanup is not started automatically.
func New[K comparable, V any](
options ...Option,
) (*Cache[K, V], error) {
) *Cache[K, V] {
return newCache[K, V](nil, options...)
}

// NewWithDefaultLoader creates a Cache with the given default loader.
// NewWithLoader creates a Cache with the given default loader.
//
// The loader is used by GetOrLoad and GetOrLoadEntry when no live cache entry
// exists. Per-call loaders may be supplied through GetOrLoadFunc and
// GetOrLoadEntryFunc. Loader must not be nil.
// GetOrLoadEntryFunc. A nil loader leaves the cache without a default loader.
//
// Options, metrics, and background cleanup have the same semantics as New.
func NewWithDefaultLoader[K comparable, V any](
// Options and background cleanup have the same semantics as New.
func NewWithLoader[K comparable, V any](
loader Loader[K, V],
options ...Option,
) (*Cache[K, V], error) {
if loader == nil {
return nil, ErrNoLoader
}

return newCache[K, V](loader, options...)
) *Cache[K, V] {
return newCache(loader, options...)
}

func newCache[K comparable, V any](
loader Loader[K, V],
options ...Option,
) (*Cache[K, V], error) {
settings, err := newSettings(options...)
if err != nil {
return nil, fmt.Errorf("pacecache: %w", err)
}
) *Cache[K, V] {
settings := newSettings(options...)

store := newStorage[K, V](
settings.maxEntries,
settings.segmentCount,
settings.slidingExpiration,
)

cache := &Cache[K, V]{
return &Cache[K, V]{
loader: loader,
name: settings.name,

store: store,
states: make([]cacheState[K, V], len(store.segments)),
Expand All @@ -101,12 +97,17 @@ func newCache[K comparable, V any](
},
cleanupInterval: settings.cleanupInterval,
}
}

if err := cache.registerMetrics(settings.name, settings.metrics); err != nil {
return nil, fmt.Errorf("pacecache: register metrics: %w", err)
// Name returns the optional logical name assigned to the cache.
//
// An unnamed, nil, or zero-value Cache returns an empty string.
func (cache *Cache[K, V]) Name() string {
if cache == nil {
return ""
}

return cache, nil
return cache.name
}

// StartCleanup runs periodic expiration cleanup until StopCleanup is called.
Expand Down Expand Up @@ -170,19 +171,6 @@ func (cache *Cache[K, V]) effectiveTTL(expiration time.Duration) time.Duration {
return jitteredTTL(ttl, cache.jitter)
}

func (cache *Cache[K, V]) registerMetrics(name string, metrics Metrics) error {
if metrics == nil {
return nil
}

return metrics.Register(
metricsSource[K, V]{
name: name,
cache: cache,
},
)
}

func (cache *Cache[K, V]) initialized() bool {
return cache != nil &&
cache.store != nil &&
Expand Down
Loading
Loading