From 40f43a39ff6cc89c54fc3af1f837cb6416dc2c80 Mon Sep 17 00:00:00 2001 From: mkbeh Date: Fri, 18 Sep 2026 13:28:30 +0300 Subject: [PATCH 1/8] refactor: rework metrics registration --- .../hitratio/internal/policy/policy.go | 1 - benchmarks/performance/memory/main.go | 1 - .../performance/throughput/throughput_test.go | 1 - cache.go | 66 +- cache_test.go | 19 +- cleanup_test.go | 3 +- delete_test.go | 15 +- doc.go | 7 +- extra/paceotel/README.md | 8 +- extra/paceotel/cache.go | 121 +--- extra/paceotel/cache_test.go | 629 +++++++++++------- extra/paceotel/metrics.go | 157 ++++- extra/paceotel/metrics_test.go | 509 ++++++++++++-- extra/paceotel/options.go | 18 - extra/paceotel/options_test.go | 37 +- extra/paceotel/version.go | 31 + load_test.go | 31 +- lookup_test.go | 9 +- metrics.go | 36 +- metrics_test.go | 185 ++++-- options.go | 57 +- options_test.go | 118 ++-- set_test.go | 20 +- stats_test.go | 47 +- 24 files changed, 1423 insertions(+), 703 deletions(-) create mode 100644 extra/paceotel/version.go diff --git a/benchmarks/performance/hitratio/internal/policy/policy.go b/benchmarks/performance/hitratio/internal/policy/policy.go index 82658c4..b7fa6c9 100644 --- a/benchmarks/performance/hitratio/internal/policy/policy.go +++ b/benchmarks/performance/hitratio/internal/policy/policy.go @@ -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), ) diff --git a/benchmarks/performance/memory/main.go b/benchmarks/performance/memory/main.go index a84dbd7..8546d46 100644 --- a/benchmarks/performance/memory/main.go +++ b/benchmarks/performance/memory/main.go @@ -38,7 +38,6 @@ func main() { runtime.ReadMemStats(&before) cache, err := pacecache.New[string, string]( - "memory", pacecache.WithMaxEntries(*capacity), pacecache.WithSegmentCount(segmentCount), pacecache.WithTTL(expiration), diff --git a/benchmarks/performance/throughput/throughput_test.go b/benchmarks/performance/throughput/throughput_test.go index b08af3d..09a749b 100644 --- a/benchmarks/performance/throughput/throughput_test.go +++ b/benchmarks/performance/throughput/throughput_test.go @@ -133,7 +133,6 @@ func newThroughputCache( for range throughputPopulationAttempts { cache, err := pacecache.New[string, string]( - "throughput", pacecache.WithMaxEntries(maxEntries), pacecache.WithSegmentCount(throughputSegments), ) diff --git a/cache.go b/cache.go index 74ac090..71a1809 100644 --- a/cache.go +++ b/cache.go @@ -24,7 +24,6 @@ const ( // // Cache is safe for concurrent use. A Cache must not be copied after creation. type Cache[K comparable, V any] struct { - name string loader Loader[K, V] store *storage[K, V] @@ -33,40 +32,31 @@ type Cache[K comparable, V any] struct { cleanupPolicy cleanupPolicy cleanup *cleanupWorker[K, V] - - metrics MetricsRegistration - closeOnce sync.Once + closeOnce sync.Once ttl time.Duration jitter time.Duration } -// New creates a Cache with the given logical name. -// -// Name is used by diagnostics and metrics and must not be empty. +// New creates a Cache. // // 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 and background cleanup are disabled by default. -// -// If metrics or background cleanup are configured, Close must be called to -// release the associated resources. func New[K comparable, V any]( - name string, options ...Option, ) (*Cache[K, V], error) { - return newCache[K, V](name, nil, options...) + return newCache[K, V](nil, options...) } -// NewWithDefaultLoader creates a Cache with the given logical name and default loader. +// NewWithDefaultLoader 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. // -// Name, options, metrics, and background cleanup have the same semantics as New. +// Options, metrics, and background cleanup have the same semantics as New. func NewWithDefaultLoader[K comparable, V any]( - name string, loader Loader[K, V], options ...Option, ) (*Cache[K, V], error) { @@ -74,15 +64,14 @@ func NewWithDefaultLoader[K comparable, V any]( return nil, ErrNoLoader } - return newCache[K, V](name, loader, options...) + return newCache[K, V](loader, options...) } func newCache[K comparable, V any]( - name string, loader Loader[K, V], options ...Option, ) (*Cache[K, V], error) { - settings, err := newCacheSettings(name, options...) + settings, err := newSettings(options...) if err != nil { return nil, fmt.Errorf("pacecache: %w", err) } @@ -99,7 +88,6 @@ func newCache[K comparable, V any]( } cache := &Cache[K, V]{ - name: settings.name, loader: loader, store: store, @@ -112,7 +100,7 @@ func newCache[K comparable, V any]( jitter: settings.jitter, } - if err := cache.registerMetrics(settings.metrics); err != nil { + if err := cache.registerMetrics(settings.name, settings.metrics); err != nil { return nil, fmt.Errorf("pacecache: register metrics: %w", err) } @@ -129,24 +117,10 @@ func newCache[K comparable, V any]( return cache, nil } -// Name returns the logical cache name. -func (cache *Cache[K, V]) Name() string { - if cache == nil { - return "" - } - - return cache.name -} - -// Close releases background resources associated with the cache. Close is -// idempotent. +// Close stops background cleanup and waits for the worker to exit. // -// If background cleanup is configured, Close stops it and waits for the cleaner -// goroutine to exit. If metrics are configured, Close also releases their -// registration. -// -// Close does not clear or disable the cache. Cache operations remain available, -// but stopped background resources are not restarted. +// Close does not clear or disable the cache. Repeated calls are safe. Close is +// a no-op on a nil Cache. func (cache *Cache[K, V]) Close() { if cache == nil { return @@ -156,10 +130,6 @@ func (cache *Cache[K, V]) Close() { if cache.cleanup != nil { cache.cleanup.close() } - - if cache.metrics != nil { - cache.metrics.Close() - } }) } @@ -177,23 +147,17 @@ func (cache *Cache[K, V]) effectiveTTL(expiration time.Duration) time.Duration { return jitteredTTL(ttl, cache.jitter) } -func (cache *Cache[K, V]) registerMetrics(metrics Metrics) error { +func (cache *Cache[K, V]) registerMetrics(name string, metrics Metrics) error { if metrics == nil { return nil } - registration, err := metrics.RegisterCache( - cacheStatsProvider[K, V]{ + return metrics.Register( + metricsSource[K, V]{ + name: name, cache: cache, }, ) - if err != nil { - return err - } - - cache.metrics = registration - - return nil } func (cache *Cache[K, V]) initialized() bool { diff --git a/cache_test.go b/cache_test.go index 2b2e55d..d515802 100644 --- a/cache_test.go +++ b/cache_test.go @@ -11,9 +11,6 @@ import ( func TestNilCacheIsSafe(t *testing.T) { var cache *Cache[string, int] - if got := cache.Name(); got != "" { - t.Fatalf("nil Cache.Name() = %q, want empty", got) - } if cache.Exists("key") { t.Fatal("nil Cache.Exists() = true, want false") } @@ -84,7 +81,7 @@ func TestZeroValueCacheLoadReturnsNotInitialized(t *testing.T) { } func TestCloseIsIdempotentAndCacheRemainsUsable(t *testing.T) { - cache, err := New[string, int]("users") + cache, err := New[string, int]() if err != nil { t.Fatalf("New() error = %v", err) } @@ -140,7 +137,7 @@ func TestEffectiveTTLAndDeadlineHelpers(t *testing.T) { } func TestNewEnablesSlidingExpiration(t *testing.T) { - cache := mustNewCache[int](t, "users", WithSlidingExpiration()) + cache := mustNewCache[int](t, WithSlidingExpiration()) for index := range cache.store.segments { if !cache.store.segments[index].slidingExpiration { t.Fatalf("segment %d sliding expiration disabled", index) @@ -149,11 +146,11 @@ func TestNewEnablesSlidingExpiration(t *testing.T) { } func TestNewWrapsConfigurationError(t *testing.T) { - cache, err := New[string, int]("") + cache, err := New[string, int](WithMaxEntries(2), WithSegmentCount(3)) if cache != nil { t.Fatal("cache must be nil for invalid configuration") } - if err == nil || err.Error() != "pacecache: invalid configuration: cache name must not be empty" { + if err == nil || err.Error() != "pacecache: invalid configuration: segment count must not exceed max entries" { t.Fatalf("New() error = %v", err) } } @@ -164,7 +161,7 @@ type testCompositeKey struct { } func TestCacheSupportsInt64Keys(t *testing.T) { - cache, err := New[int64, string]("users", WithMaxEntries(8), WithSegmentCount(2)) + cache, err := New[int64, string](WithMaxEntries(8), WithSegmentCount(2)) if err != nil { t.Fatalf("New() error = %v", err) } @@ -189,7 +186,7 @@ func TestCacheSupportsInt64Keys(t *testing.T) { } func TestCacheSupportsComparableStructKeys(t *testing.T) { - cache, err := New[testCompositeKey, int]("users", WithMaxEntries(8), WithSegmentCount(2)) + cache, err := New[testCompositeKey, int](WithMaxEntries(8), WithSegmentCount(2)) if err != nil { t.Fatalf("New() error = %v", err) } @@ -255,10 +252,10 @@ func (ctx *observedWaitContext) cancel() { const testTimeout = 5 * time.Second -func mustNewCache[V any](t *testing.T, name string, options ...Option) *Cache[string, V] { +func mustNewCache[V any](t *testing.T, options ...Option) *Cache[string, V] { t.Helper() - cache, err := New[string, V](name, options...) + cache, err := New[string, V](options...) if err != nil { t.Fatalf("New() error = %v", err) } diff --git a/cleanup_test.go b/cleanup_test.go index 2e19c8a..71dac24 100644 --- a/cleanup_test.go +++ b/cleanup_test.go @@ -87,7 +87,7 @@ func TestCleanupWorkerBackgroundRemovesExpiredEntry(t *testing.T) { } func TestCacheCloseStopsBackgroundCleanup(t *testing.T) { - cache, err := New[string, int]("users", WithCleanupInterval(time.Millisecond)) + cache, err := New[string, int](WithCleanupInterval(time.Millisecond)) if err != nil { t.Fatalf("New() error = %v", err) } @@ -190,7 +190,6 @@ func TestCleanupWorkerRunSchedulesContinuationForBacklog(t *testing.T) { func TestCacheCleanupWorkerUsesConfiguredLimits(t *testing.T) { cache, err := New[string, int]( - "users", WithCleanupInterval(time.Hour), WithCleanupBatchSize(7), WithCleanupEntryBudget(11), diff --git a/delete_test.go b/delete_test.go index a5326b2..cf1ef7e 100644 --- a/delete_test.go +++ b/delete_test.go @@ -9,7 +9,7 @@ import ( ) func TestCacheGetAndDelete(t *testing.T) { - cache := mustNewCache[int](t, "users", WithMaxEntries(8), WithSegmentCount(1)) + cache := mustNewCache[int](t, WithMaxEntries(8), WithSegmentCount(1)) cache.Set("key", 42, NoExpiration) before := cache.Stats() @@ -40,7 +40,7 @@ func TestCacheGetAndDelete(t *testing.T) { } func TestCacheGetAndDeleteIsAtomic(t *testing.T) { - cache := mustNewCache[int](t, "users", WithMaxEntries(8), WithSegmentCount(1)) + cache := mustNewCache[int](t, WithMaxEntries(8), WithSegmentCount(1)) cache.Set("key", 42, NoExpiration) const callers = 32 @@ -94,7 +94,7 @@ func TestCacheGetAndDeleteIsAtomic(t *testing.T) { } func TestCacheGetAndDeleteExpiredEntry(t *testing.T) { - cache := mustNewCache[int](t, "users", WithMaxEntries(8), WithSegmentCount(1)) + cache := mustNewCache[int](t, WithMaxEntries(8), WithSegmentCount(1)) index := cache.store.segmentIndex("expired") cache.store.setAt( @@ -126,7 +126,7 @@ func TestCacheGetAndDeleteExpiredEntry(t *testing.T) { } func TestCacheGetAndDeleteSupersedesInflightLoad(t *testing.T) { - cache := mustNewCache[int](t, "users", WithMaxEntries(8), WithSegmentCount(1)) + cache := mustNewCache[int](t, WithMaxEntries(8), WithSegmentCount(1)) started := make(chan struct{}) release := make(chan struct{}) @@ -200,7 +200,7 @@ func TestDeleteMultipleKeysAndDuplicates(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - cache := mustNewCache[int](t, "users", test.options...) + cache := mustNewCache[int](t, test.options...) cache.Set("a", 1, NoExpiration) cache.Set("b", 2, NoExpiration) cache.Set("c", 3, NoExpiration) @@ -224,7 +224,7 @@ func TestDeleteMultipleKeysAndDuplicates(t *testing.T) { } func TestConcurrentMultiKeyDeletionLockOrderDoesNotDeadlock(t *testing.T) { - cache := mustNewCache[int](t, "users", WithMaxEntries(16), WithSegmentCount(8)) + cache := mustNewCache[int](t, WithMaxEntries(16), WithSegmentCount(8)) cache.Set("a", 1, NoExpiration) cache.Set("b", 2, NoExpiration) @@ -243,7 +243,7 @@ func TestConcurrentMultiKeyDeletionLockOrderDoesNotDeadlock(t *testing.T) { } func TestCacheClear(t *testing.T) { - cache := mustNewCache[int](t, "users", WithMaxEntries(8), WithSegmentCount(2)) + cache := mustNewCache[int](t, WithMaxEntries(8), WithSegmentCount(2)) cache.Set("a", 1, NoExpiration) cache.Set("b", 2, NoExpiration) @@ -279,7 +279,6 @@ func TestCacheClear(t *testing.T) { func TestCacheDeleteExpired(t *testing.T) { store := newStorageWithExpirationResolution[string, int](4, 1, time.Nanosecond) cache := &Cache[string, int]{ - name: "test", store: store, states: make([]cacheState[string, int], 1), stats: newStatsCollector(1), diff --git a/doc.go b/doc.go index d4c00aa..186e793 100644 --- a/doc.go +++ b/doc.go @@ -5,12 +5,17 @@ // loading with configurable default or per-call loaders, duplicate load // suppression, and explicit removal. // +// Background expiration cleanup is optional and can be enabled with +// WithCleanupInterval. Cache.Close stops the cleanup worker and waits for it to +// exit. Cache operations remain available after Close. +// // Cache mutations act as publication barriers for concurrent loads, preventing // superseded loader results from overwriting newer cache state. // // Cache statistics are collected locally and exposed through Cache.Stats. // Optional metrics integrations register during New and observe those snapshots -// without adding telemetry calls to the cache request path. +// without adding telemetry calls to the cache request path. Metrics integrations +// own the lifecycle of their registrations. // // The cache is local to one application process. It does not provide // distributed cache coherence between application instances. diff --git a/extra/paceotel/README.md b/extra/paceotel/README.md index b9ad83a..c299258 100644 --- a/extra/paceotel/README.md +++ b/extra/paceotel/README.md @@ -14,6 +14,7 @@ go get github.com/mkbeh/pacecache/extra/paceotel ## Usage + ```go import ( "context" @@ -30,14 +31,15 @@ defer provider.Shutdown(context.Background()) metrics := paceotel.New( paceotel.WithMeterProvider(provider), ) +defer metrics.Unregister() // Attach metrics when creating the cache. cache, _ := pacecache.New[string, string]( - "users", + pacecache.WithName("users"), pacecache.WithMetrics(metrics), ) -defer cache.Close() ``` + -For a complete runnable setup using the stdout exporter, see the [example](../../examples/otel). \ No newline at end of file +For a complete runnable setup using the stdout exporter, see the [example](../../examples/otel). diff --git a/extra/paceotel/cache.go b/extra/paceotel/cache.go index 62928ac..150c4f2 100644 --- a/extra/paceotel/cache.go +++ b/extra/paceotel/cache.go @@ -1,13 +1,10 @@ package paceotel import ( - "context" - "errors" "fmt" "slices" "github.com/mkbeh/pacecache" - "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/metric" ) @@ -49,7 +46,7 @@ const ( removalOperationClear = "clear" ) -type cacheMetricInstruments struct { +type metricInstruments struct { entryCount metric.Int64ObservableGauge entryLimit metric.Int64ObservableGauge segmentCount metric.Int64ObservableGauge @@ -67,7 +64,7 @@ type cacheMetricInstruments struct { expirationCount metric.Int64ObservableCounter } -type cacheMetricAttributes struct { +type metricAttributes struct { base metric.ObserveOption hit metric.ObserveOption @@ -81,66 +78,10 @@ type cacheMetricAttributes struct { clearOperation metric.ObserveOption } -// RegisterCache registers OpenTelemetry metrics for one cache. -func (metrics *Metrics) RegisterCache( - cache pacecache.StatsProvider, -) (pacecache.MetricsRegistration, error) { - if metrics == nil { - return nil, errors.New("paceotel: metrics is nil") - } - - if cache == nil { - return nil, errors.New("paceotel: cache is nil") - } - - name := cache.Name() - if name == "" { - return nil, errors.New("paceotel: cache name is empty") - } - - meterProvider := metrics.meterProvider - if meterProvider == nil { - meterProvider = otel.GetMeterProvider() - } - - return registerCacheMetrics(cache, name, meterProvider) -} - -func registerCacheMetrics( - cache pacecache.StatsProvider, - name string, - provider metric.MeterProvider, -) (pacecache.MetricsRegistration, error) { - meter := provider.Meter(instrumentationName) - - instruments, err := newCacheMetricInstruments(meter) - if err != nil { - return nil, err - } - - attributes := newCacheMetricAttributes(name) - - registration, err := meter.RegisterCallback( - func(_ context.Context, observer metric.Observer) error { - instruments.observe(observer, cache.Stats(), attributes) - - return nil - }, - instruments.observables()..., - ) - if err != nil { - return nil, fmt.Errorf("paceotel: register metrics callback: %w", err) - } - - return &metricsRegistration{ - registration: registration, - }, nil -} - -func (instruments cacheMetricInstruments) observe( +func (instruments metricInstruments) observe( observer metric.Observer, stats pacecache.Stats, - attributes cacheMetricAttributes, + attributes metricAttributes, ) { observer.ObserveInt64( instruments.entryCount, @@ -257,7 +198,7 @@ func (instruments cacheMetricInstruments) observe( ) } -func (instruments cacheMetricInstruments) observables() []metric.Observable { +func (instruments metricInstruments) observables() []metric.Observable { return []metric.Observable{ instruments.entryCount, instruments.entryLimit, @@ -277,10 +218,8 @@ func (instruments cacheMetricInstruments) observables() []metric.Observable { } } -func newCacheMetricInstruments( - meter metric.Meter, -) (cacheMetricInstruments, error) { - var instruments cacheMetricInstruments +func newMetricInstruments(meter metric.Meter) (metricInstruments, error) { + var instruments metricInstruments var err error instruments.entryCount, err = meter.Int64ObservableGauge( @@ -291,7 +230,7 @@ func newCacheMetricInstruments( metric.WithUnit("{entry}"), ) if err != nil { - return cacheMetricInstruments{}, + return metricInstruments{}, newMetricError(entryCountMetricName, err) } @@ -303,7 +242,7 @@ func newCacheMetricInstruments( metric.WithUnit("{entry}"), ) if err != nil { - return cacheMetricInstruments{}, + return metricInstruments{}, newMetricError(entryLimitMetricName, err) } @@ -315,7 +254,7 @@ func newCacheMetricInstruments( metric.WithUnit("{segment}"), ) if err != nil { - return cacheMetricInstruments{}, + return metricInstruments{}, newMetricError(segmentCountMetricName, err) } @@ -327,7 +266,7 @@ func newCacheMetricInstruments( metric.WithUnit("{lookup}"), ) if err != nil { - return cacheMetricInstruments{}, + return metricInstruments{}, newMetricError(lookupCountMetricName, err) } @@ -339,7 +278,7 @@ func newCacheMetricInstruments( metric.WithUnit("{load}"), ) if err != nil { - return cacheMetricInstruments{}, + return metricInstruments{}, newMetricError(loadCountMetricName, err) } @@ -351,7 +290,7 @@ func newCacheMetricInstruments( metric.WithUnit("s"), ) if err != nil { - return cacheMetricInstruments{}, + return metricInstruments{}, newMetricError(loadTimeMetricName, err) } @@ -363,7 +302,7 @@ func newCacheMetricInstruments( metric.WithUnit("{request}"), ) if err != nil { - return cacheMetricInstruments{}, + return metricInstruments{}, newMetricError(loadSharedCountMetricName, err) } @@ -375,7 +314,7 @@ func newCacheMetricInstruments( metric.WithUnit("{load}"), ) if err != nil { - return cacheMetricInstruments{}, + return metricInstruments{}, newMetricError(loadSupersededCountMetricName, err) } @@ -387,7 +326,7 @@ func newCacheMetricInstruments( metric.WithUnit("{entry}"), ) if err != nil { - return cacheMetricInstruments{}, + return metricInstruments{}, newMetricError(removedCountMetricName, err) } @@ -399,7 +338,7 @@ func newCacheMetricInstruments( metric.WithUnit("{cleanup}"), ) if err != nil { - return cacheMetricInstruments{}, + return metricInstruments{}, newMetricError(cleanupCountMetricName, err) } @@ -411,7 +350,7 @@ func newCacheMetricInstruments( metric.WithUnit("{run}"), ) if err != nil { - return cacheMetricInstruments{}, + return metricInstruments{}, newMetricError(cleanupWorkerRunCountMetricName, err) } @@ -423,7 +362,7 @@ func newCacheMetricInstruments( metric.WithUnit("{run}"), ) if err != nil { - return cacheMetricInstruments{}, + return metricInstruments{}, newMetricError(cleanupWorkerPendingCountMetricName, err) } @@ -435,7 +374,7 @@ func newCacheMetricInstruments( metric.WithUnit("s"), ) if err != nil { - return cacheMetricInstruments{}, + return metricInstruments{}, newMetricError(cleanupWorkerTimeMetricName, err) } @@ -447,7 +386,7 @@ func newCacheMetricInstruments( metric.WithUnit("{entry}"), ) if err != nil { - return cacheMetricInstruments{}, + return metricInstruments{}, newMetricError(evictionCountMetricName, err) } @@ -459,19 +398,21 @@ func newCacheMetricInstruments( metric.WithUnit("{entry}"), ) if err != nil { - return cacheMetricInstruments{}, + return metricInstruments{}, newMetricError(expirationCountMetricName, err) } return instruments, nil } -func newCacheMetricAttributes(name string) cacheMetricAttributes { - base := []attribute.KeyValue{ - attribute.String( - cacheNameAttribute, - name, - ), +func newMetricAttributes(name string) metricAttributes { + var base []attribute.KeyValue + + if name != "" { + base = append( + base, + attribute.String(cacheNameAttribute, name), + ) } option := func(extra ...attribute.KeyValue) metric.ObserveOption { @@ -481,7 +422,7 @@ func newCacheMetricAttributes(name string) cacheMetricAttributes { ) } - return cacheMetricAttributes{ + return metricAttributes{ base: option(), hit: option( diff --git a/extra/paceotel/cache_test.go b/extra/paceotel/cache_test.go index 73f19f5..f594fcd 100644 --- a/extra/paceotel/cache_test.go +++ b/extra/paceotel/cache_test.go @@ -3,6 +3,7 @@ package paceotel import ( "context" "errors" + "sync/atomic" "testing" "time" @@ -12,77 +13,72 @@ import ( "go.opentelemetry.io/otel/sdk/metric/metricdata" ) -type statsProviderStub struct { - name string - stats pacecache.Stats -} - -func (provider *statsProviderStub) Name() string { - if provider == nil { - return "" - } +const ( + wantScopeName = "github.com/mkbeh/pacecache/extra/paceotel" + + wantEntryCountMetricName = "pacecache.entry.count" + wantEntryLimitMetricName = "pacecache.entry.limit" + wantSegmentCountMetricName = "pacecache.segment.count" + wantLookupCountMetricName = "pacecache.lookup.count" + wantLoadCountMetricName = "pacecache.load.count" + wantLoadTimeMetricName = "pacecache.load.time" + wantLoadSharedCountMetricName = "pacecache.load.shared.count" + wantLoadSupersededCountMetricName = "pacecache.load.superseded.count" + wantRemovedCountMetricName = "pacecache.entry.removed.count" + wantCleanupCountMetricName = "pacecache.cleanup.count" + wantCleanupWorkerRunCountMetricName = "pacecache.cleanup.worker.run.count" + wantCleanupWorkerPendingCountMetricName = "pacecache.cleanup.worker.pending.count" + wantCleanupWorkerTimeMetricName = "pacecache.cleanup.worker.time" + wantEvictionCountMetricName = "pacecache.entry.eviction.count" + wantExpirationCountMetricName = "pacecache.entry.expiration.count" + + wantCacheNameAttribute = "pacecache.name" + wantLookupResultAttribute = "pacecache.lookup.result" + wantLoadResultAttribute = "pacecache.load.result" + wantRemovalOperationAttribute = "pacecache.removal.operation" +) - return provider.name -} +func TestMetricsSchema(t *testing.T) { + metrics, reader := newTestMetrics(t) -func (provider *statsProviderStub) Stats() pacecache.Stats { - if provider == nil { - return pacecache.Stats{} + if err := metrics.Register(&metricsSourceStub{name: "users"}); err != nil { + t.Fatalf("Register() error = %v", err) } - return provider.stats -} - -func TestRegisterCacheValidation(t *testing.T) { - provider := &statsProviderStub{name: "users"} - - t.Run("nil metrics", func(t *testing.T) { - var metrics *Metrics - - registration, err := metrics.RegisterCache(provider) - if registration != nil { - t.Fatalf("RegisterCache() registration = %v, want nil", registration) - } - if err == nil || err.Error() != "paceotel: metrics is nil" { - t.Fatalf("RegisterCache() error = %v, want metrics is nil", err) - } - }) - - t.Run("nil cache", func(t *testing.T) { - metrics := New() - - registration, err := metrics.RegisterCache(nil) - if registration != nil { - t.Fatalf("RegisterCache() registration = %v, want nil", registration) - } - if err == nil || err.Error() != "paceotel: cache is nil" { - t.Fatalf("RegisterCache() error = %v, want cache is nil", err) - } - }) + collected := collectMetricsByName(t, collectTestMetrics(t, reader)) + + expected := map[string]string{ + wantEntryCountMetricName: "{entry}", + wantEntryLimitMetricName: "{entry}", + wantSegmentCountMetricName: "{segment}", + wantLookupCountMetricName: "{lookup}", + wantLoadCountMetricName: "{load}", + wantLoadTimeMetricName: "s", + wantLoadSharedCountMetricName: "{request}", + wantLoadSupersededCountMetricName: "{load}", + wantRemovedCountMetricName: "{entry}", + wantCleanupCountMetricName: "{cleanup}", + wantCleanupWorkerRunCountMetricName: "{run}", + wantCleanupWorkerPendingCountMetricName: "{run}", + wantCleanupWorkerTimeMetricName: "s", + wantEvictionCountMetricName: "{entry}", + wantExpirationCountMetricName: "{entry}", + } - t.Run("blank cache name", func(t *testing.T) { - metrics := New() + if len(collected) != len(expected) { + t.Fatalf("collected metrics = %d, want %d", len(collected), len(expected)) + } - registration, err := metrics.RegisterCache(&statsProviderStub{}) - if registration != nil { - t.Fatalf("RegisterCache() registration = %v, want nil", registration) - } - if err == nil || err.Error() != "paceotel: cache name is empty" { - t.Fatalf("RegisterCache() error = %v, want cache name is empty", err) + for name, unit := range expected { + current := requireMetric(t, collected, name) + if current.Unit != unit { + t.Fatalf("metric %q unit = %q, want %q", name, current.Unit, unit) } - }) + } } -func TestRegisterCacheCollectsMetrics(t *testing.T) { - reader := sdkmetric.NewManualReader() - meterProvider := sdkmetric.NewMeterProvider( - sdkmetric.WithReader(reader), - ) - t.Cleanup(func() { - if err := meterProvider.Shutdown(context.Background()); err != nil { - t.Fatalf("Shutdown() error = %v", err) - } - }) +func TestMetricsCollectsSource(t *testing.T) { + metrics, reader := newTestMetrics(t) stats := pacecache.Stats{ EntryCount: 3, @@ -111,204 +107,326 @@ func TestRegisterCacheCollectsMetrics(t *testing.T) { ExpirationCount: 14, } - metrics := New( - WithMeterProvider(meterProvider), - ) + var statsCalls atomic.Int64 - registration, err := metrics.RegisterCache( - &statsProviderStub{ - name: "users", - stats: stats, - }, - ) - if err != nil { - t.Fatalf("RegisterCache() error = %v", err) - } - if registration == nil { - t.Fatal("RegisterCache() registration = nil") - } - t.Cleanup(registration.Close) + if err := metrics.Register( + &metricsSourceStub{ + name: "users", + statsFn: func() pacecache.Stats { + statsCalls.Add(1) - var resourceMetrics metricdata.ResourceMetrics - if err := reader.Collect( - context.Background(), - &resourceMetrics, + return stats + }, + }, ); err != nil { - t.Fatalf("Collect() error = %v", err) + t.Fatalf("Register() error = %v", err) } - collected := collectMetricsByName(t, resourceMetrics) + collected := collectMetricsByName(t, collectTestMetrics(t, reader)) + + if got := statsCalls.Load(); got != 1 { + t.Fatalf("Stats() calls = %d, want 1", got) + } - requireInt64MetricPoint( + requireInt64GaugePoint( t, collected, - entryCountMetricName, - map[string]string{cacheNameAttribute: "users"}, + wantEntryCountMetricName, + map[string]string{wantCacheNameAttribute: "users"}, 3, ) - requireInt64MetricPoint( + requireInt64GaugePoint( t, collected, - entryLimitMetricName, - map[string]string{cacheNameAttribute: "users"}, + wantEntryLimitMetricName, + map[string]string{wantCacheNameAttribute: "users"}, 128, ) - requireInt64MetricPoint( + requireInt64GaugePoint( t, collected, - segmentCountMetricName, - map[string]string{cacheNameAttribute: "users"}, + wantSegmentCountMetricName, + map[string]string{wantCacheNameAttribute: "users"}, 4, ) - requireInt64MetricPoint( + lookupPoints := int64CounterPoints( + t, + requireMetric(t, collected, wantLookupCountMetricName), + ) + if len(lookupPoints) != 2 { + t.Fatalf("lookup count points = %d, want 2", len(lookupPoints)) + } + requireInt64CounterPoint( t, collected, - lookupCountMetricName, + wantLookupCountMetricName, map[string]string{ - cacheNameAttribute: "users", - lookupResultAttribute: lookupResultHit, + wantCacheNameAttribute: "users", + wantLookupResultAttribute: "hit", }, 11, ) - requireInt64MetricPoint( + requireInt64CounterPoint( t, collected, - lookupCountMetricName, + wantLookupCountMetricName, map[string]string{ - cacheNameAttribute: "users", - lookupResultAttribute: lookupResultMiss, + wantCacheNameAttribute: "users", + wantLookupResultAttribute: "miss", }, 7, ) - requireNoInt64MetricPoint( + + loadPoints := int64CounterPoints( t, - collected, - lookupCountMetricName, - map[string]string{ - cacheNameAttribute: "users", - lookupResultAttribute: "negative_hit", - }, + requireMetric(t, collected, wantLoadCountMetricName), ) - - requireInt64MetricPoint( + if len(loadPoints) != 3 { + t.Fatalf("load count points = %d, want 3", len(loadPoints)) + } + requireInt64CounterPoint( t, collected, - loadCountMetricName, + wantLoadCountMetricName, map[string]string{ - cacheNameAttribute: "users", - loadResultAttribute: loadResultFound, + wantCacheNameAttribute: "users", + wantLoadResultAttribute: "found", }, 5, ) - requireInt64MetricPoint( + requireInt64CounterPoint( t, collected, - loadCountMetricName, + wantLoadCountMetricName, map[string]string{ - cacheNameAttribute: "users", - loadResultAttribute: loadResultNotFound, + wantCacheNameAttribute: "users", + wantLoadResultAttribute: "not_found", }, 3, ) - requireInt64MetricPoint( + requireInt64CounterPoint( t, collected, - loadCountMetricName, + wantLoadCountMetricName, map[string]string{ - cacheNameAttribute: "users", - loadResultAttribute: loadResultError, + wantCacheNameAttribute: "users", + wantLoadResultAttribute: "error", }, 2, ) - requireFloat64MetricPoint( + requireFloat64CounterPoint( t, collected, - loadTimeMetricName, - map[string]string{cacheNameAttribute: "users"}, + wantLoadTimeMetricName, + map[string]string{wantCacheNameAttribute: "users"}, 1.5, ) - requireInt64MetricPoint( + requireInt64CounterPoint( t, collected, - loadSharedCountMetricName, - map[string]string{cacheNameAttribute: "users"}, + wantLoadSharedCountMetricName, + map[string]string{wantCacheNameAttribute: "users"}, 4, ) - requireInt64MetricPoint( + requireInt64CounterPoint( t, collected, - loadSupersededCountMetricName, - map[string]string{cacheNameAttribute: "users"}, + wantLoadSupersededCountMetricName, + map[string]string{wantCacheNameAttribute: "users"}, 1, ) - requireInt64MetricPoint( + removedPoints := int64CounterPoints( + t, + requireMetric(t, collected, wantRemovedCountMetricName), + ) + if len(removedPoints) != 2 { + t.Fatalf("removed count points = %d, want 2", len(removedPoints)) + } + requireInt64CounterPoint( t, collected, - removedCountMetricName, + wantRemovedCountMetricName, map[string]string{ - cacheNameAttribute: "users", - removalOperationAttribute: removalOperationDelete, + wantCacheNameAttribute: "users", + wantRemovalOperationAttribute: "delete", }, 8, ) - requireInt64MetricPoint( + requireInt64CounterPoint( t, collected, - removedCountMetricName, + wantRemovedCountMetricName, map[string]string{ - cacheNameAttribute: "users", - removalOperationAttribute: removalOperationClear, + wantCacheNameAttribute: "users", + wantRemovalOperationAttribute: "clear", }, 9, ) - requireInt64MetricPoint( + requireInt64CounterPoint( t, collected, - cleanupCountMetricName, - map[string]string{cacheNameAttribute: "users"}, + wantCleanupCountMetricName, + map[string]string{wantCacheNameAttribute: "users"}, 10, ) - requireInt64MetricPoint( + requireInt64CounterPoint( t, collected, - cleanupWorkerRunCountMetricName, - map[string]string{cacheNameAttribute: "users"}, + wantCleanupWorkerRunCountMetricName, + map[string]string{wantCacheNameAttribute: "users"}, 11, ) - requireInt64MetricPoint( + requireInt64CounterPoint( t, collected, - cleanupWorkerPendingCountMetricName, - map[string]string{cacheNameAttribute: "users"}, + wantCleanupWorkerPendingCountMetricName, + map[string]string{wantCacheNameAttribute: "users"}, 12, ) - requireFloat64MetricPoint( + requireFloat64CounterPoint( t, collected, - cleanupWorkerTimeMetricName, - map[string]string{cacheNameAttribute: "users"}, + wantCleanupWorkerTimeMetricName, + map[string]string{wantCacheNameAttribute: "users"}, 2.5, ) - requireInt64MetricPoint( + requireInt64CounterPoint( t, collected, - evictionCountMetricName, - map[string]string{cacheNameAttribute: "users"}, + wantEvictionCountMetricName, + map[string]string{wantCacheNameAttribute: "users"}, 13, ) - requireInt64MetricPoint( + requireInt64CounterPoint( t, collected, - expirationCountMetricName, - map[string]string{cacheNameAttribute: "users"}, + wantExpirationCountMetricName, + map[string]string{wantCacheNameAttribute: "users"}, 14, ) } +func TestMetricsCollectsMultipleSources(t *testing.T) { + metrics, reader := newTestMetrics(t) + + if err := metrics.Register( + &metricsSourceStub{ + name: "users", + stats: pacecache.Stats{ + EntryCount: 3, + MaxEntries: 128, + HitCount: 11, + }, + }, + ); err != nil { + t.Fatalf("Register(users) error = %v", err) + } + + if err := metrics.Register( + &metricsSourceStub{ + name: "sessions", + stats: pacecache.Stats{ + EntryCount: 7, + MaxEntries: 64, + MissCount: 5, + }, + }, + ); err != nil { + t.Fatalf("Register(sessions) error = %v", err) + } + + collected := collectMetricsByName(t, collectTestMetrics(t, reader)) + + entryPoints := int64GaugePoints( + t, + requireMetric(t, collected, wantEntryCountMetricName), + ) + if len(entryPoints) != 2 { + t.Fatalf("entry count points = %d, want 2", len(entryPoints)) + } + + requireInt64GaugePoint( + t, + collected, + wantEntryCountMetricName, + map[string]string{wantCacheNameAttribute: "users"}, + 3, + ) + requireInt64GaugePoint( + t, + collected, + wantEntryCountMetricName, + map[string]string{wantCacheNameAttribute: "sessions"}, + 7, + ) + requireInt64GaugePoint( + t, + collected, + wantEntryLimitMetricName, + map[string]string{wantCacheNameAttribute: "users"}, + 128, + ) + requireInt64GaugePoint( + t, + collected, + wantEntryLimitMetricName, + map[string]string{wantCacheNameAttribute: "sessions"}, + 64, + ) + requireInt64CounterPoint( + t, + collected, + wantLookupCountMetricName, + map[string]string{ + wantCacheNameAttribute: "users", + wantLookupResultAttribute: "hit", + }, + 11, + ) + requireInt64CounterPoint( + t, + collected, + wantLookupCountMetricName, + map[string]string{ + wantCacheNameAttribute: "sessions", + wantLookupResultAttribute: "miss", + }, + 5, + ) +} + +func TestMetricsCollectsUnnamedSource(t *testing.T) { + metrics, reader := newTestMetrics(t) + + if err := metrics.Register( + &metricsSourceStub{ + stats: pacecache.Stats{EntryCount: 3}, + }, + ); err != nil { + t.Fatalf("Register() error = %v", err) + } + + collected := collectMetricsByName(t, collectTestMetrics(t, reader)) + points := int64GaugePoints( + t, + requireMetric(t, collected, wantEntryCountMetricName), + ) + + if len(points) != 1 { + t.Fatalf("entry count points = %d, want 1", len(points)) + } + if points[0].Value != 3 { + t.Fatalf("entry count = %d, want 3", points[0].Value) + } + if got := points[0].Attributes.Len(); got != 0 { + t.Fatalf("unnamed source attributes = %d, want 0", got) + } +} + func TestNewMetricError(t *testing.T) { sentinel := errors.New("instrument failed") @@ -323,6 +441,20 @@ func TestNewMetricError(t *testing.T) { } } +func collectTestMetrics( + t *testing.T, + reader *sdkmetric.ManualReader, +) metricdata.ResourceMetrics { + t.Helper() + + var resourceMetrics metricdata.ResourceMetrics + if err := reader.Collect(context.Background(), &resourceMetrics); err != nil { + t.Fatalf("Collect() error = %v", err) + } + + return resourceMetrics +} + func collectMetricsByName( t *testing.T, resourceMetrics metricdata.ResourceMetrics, @@ -330,53 +462,63 @@ func collectMetricsByName( t.Helper() collected := make(map[string]metricdata.Metrics) + foundScope := false - var foundScope bool for _, scopeMetrics := range resourceMetrics.ScopeMetrics { - if scopeMetrics.Scope.Name != instrumentationName { + if scopeMetrics.Scope.Name != wantScopeName { continue } + if foundScope { + t.Fatalf("instrumentation scope %q collected more than once", wantScopeName) + } foundScope = true + for _, current := range scopeMetrics.Metrics { + if _, exists := collected[current.Name]; exists { + t.Fatalf("metric %q collected more than once", current.Name) + } + collected[current.Name] = current } } if !foundScope { - t.Fatalf( - "instrumentation scope %q not found", - instrumentationName, - ) + t.Fatalf("instrumentation scope %q not found", wantScopeName) } - expected := []string{ - entryCountMetricName, - entryLimitMetricName, - segmentCountMetricName, - lookupCountMetricName, - loadCountMetricName, - loadTimeMetricName, - loadSharedCountMetricName, - loadSupersededCountMetricName, - removedCountMetricName, - cleanupCountMetricName, - cleanupWorkerRunCountMetricName, - cleanupWorkerPendingCountMetricName, - cleanupWorkerTimeMetricName, - evictionCountMetricName, - expirationCountMetricName, - } - for _, name := range expected { - if _, ok := collected[name]; !ok { - t.Fatalf("metric %q not collected", name) - } + return collected +} + +func requireMetric( + t *testing.T, + collected map[string]metricdata.Metrics, + name string, +) metricdata.Metrics { + t.Helper() + + current, ok := collected[name] + if !ok { + t.Fatalf("metric %q not collected", name) } - return collected + return current +} + +func requireInt64GaugePoint( + t *testing.T, + collected map[string]metricdata.Metrics, + name string, + attributes map[string]string, + want int64, +) { + t.Helper() + + points := int64GaugePoints(t, requireMetric(t, collected, name)) + requireInt64Point(t, name, points, attributes, want) } -func requireInt64MetricPoint( +func requireInt64CounterPoint( t *testing.T, collected map[string]metricdata.Metrics, name string, @@ -385,7 +527,20 @@ func requireInt64MetricPoint( ) { t.Helper() - points := int64MetricPoints(t, collected[name]) + points := int64CounterPoints(t, requireMetric(t, collected, name)) + requireInt64Point(t, name, points, attributes, want) +} + +func requireFloat64CounterPoint( + t *testing.T, + collected map[string]metricdata.Metrics, + name string, + attributes map[string]string, + want float64, +) { + t.Helper() + + points := float64CounterPoints(t, requireMetric(t, collected, name)) for _, point := range points { if !metricAttributesMatch(point.Attributes, attributes) { continue @@ -393,7 +548,7 @@ func requireInt64MetricPoint( if point.Value != want { t.Fatalf( - "metric %q value = %d, want %d; attributes=%v", + "metric %q value = %v, want %v; attributes=%v", name, point.Value, want, @@ -411,36 +566,15 @@ func requireInt64MetricPoint( ) } -func requireNoInt64MetricPoint( +func requireInt64Point( t *testing.T, - collected map[string]metricdata.Metrics, - name string, - attributes map[string]string, -) { - t.Helper() - - points := int64MetricPoints(t, collected[name]) - for _, point := range points { - if metricAttributesMatch(point.Attributes, attributes) { - t.Fatalf( - "metric %q unexpectedly contains attributes %v", - name, - attributes, - ) - } - } -} - -func requireFloat64MetricPoint( - t *testing.T, - collected map[string]metricdata.Metrics, name string, + points []metricdata.DataPoint[int64], attributes map[string]string, - want float64, + want int64, ) { t.Helper() - points := float64MetricPoints(t, collected[name]) for _, point := range points { if !metricAttributesMatch(point.Attributes, attributes) { continue @@ -448,7 +582,7 @@ func requireFloat64MetricPoint( if point.Value != want { t.Fatalf( - "metric %q value = %v, want %v; attributes=%v", + "metric %q value = %d, want %d; attributes=%v", name, point.Value, want, @@ -466,52 +600,87 @@ func requireFloat64MetricPoint( ) } -func int64MetricPoints( +func int64GaugePoints( + t *testing.T, + current metricdata.Metrics, +) []metricdata.DataPoint[int64] { + t.Helper() + + data, ok := current.Data.(metricdata.Gauge[int64]) + if !ok { + t.Fatalf( + "metric %q data type = %T, want int64 gauge", + current.Name, + current.Data, + ) + } + + return data.DataPoints +} + +func int64CounterPoints( t *testing.T, current metricdata.Metrics, ) []metricdata.DataPoint[int64] { t.Helper() - switch data := current.Data.(type) { - case metricdata.Gauge[int64]: - return data.DataPoints - case metricdata.Sum[int64]: - return data.DataPoints - default: + data, ok := current.Data.(metricdata.Sum[int64]) + if !ok { t.Fatalf( - "metric %q data type = %T, want int64 gauge or sum", + "metric %q data type = %T, want int64 sum", current.Name, current.Data, ) - return nil } + if !data.IsMonotonic { + t.Fatalf("metric %q is not monotonic", current.Name) + } + + return data.DataPoints } -func float64MetricPoints( +func float64CounterPoints( t *testing.T, current metricdata.Metrics, ) []metricdata.DataPoint[float64] { t.Helper() - switch data := current.Data.(type) { - case metricdata.Gauge[float64]: - return data.DataPoints - case metricdata.Sum[float64]: - return data.DataPoints - default: + data, ok := current.Data.(metricdata.Sum[float64]) + if !ok { t.Fatalf( - "metric %q data type = %T, want float64 gauge or sum", + "metric %q data type = %T, want float64 sum", current.Name, current.Data, ) - return nil } + if !data.IsMonotonic { + t.Fatalf("metric %q is not monotonic", current.Name) + } + + return data.DataPoints +} + +func hasMetricAttributes( + points []metricdata.DataPoint[int64], + want map[string]string, +) bool { + for _, point := range points { + if metricAttributesMatch(point.Attributes, want) { + return true + } + } + + return false } func metricAttributesMatch( set attribute.Set, want map[string]string, ) bool { + if set.Len() != len(want) { + return false + } + for key, value := range want { current, ok := set.Value(attribute.Key(key)) if !ok || current.AsString() != value { diff --git a/extra/paceotel/metrics.go b/extra/paceotel/metrics.go index f2f63fd..61c231d 100644 --- a/extra/paceotel/metrics.go +++ b/extra/paceotel/metrics.go @@ -1,36 +1,167 @@ package paceotel import ( + "context" + "errors" "fmt" + "sync" "github.com/mkbeh/pacecache" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/metric" ) -const instrumentationName = "github.com/mkbeh/pacecache/extra/paceotel" - // Metrics exports pacecache statistics through OpenTelemetry. // -// Metrics is immutable and safe to reuse across multiple caches. +// Metrics is safe to reuse across multiple caches. Each registered source must +// have a unique name within a Metrics instance; only one unnamed source may be +// registered. Unregister removes the OpenTelemetry callbacks for all sources. type Metrics struct { - meterProvider metric.MeterProvider + meter metric.Meter + + mu sync.Mutex + instruments *metricInstruments + registrations map[string]metric.Registration + closed bool } var _ pacecache.Metrics = (*Metrics)(nil) -type metricsRegistration struct { - registration metric.Registration +// New creates an OpenTelemetry metrics implementation. +// +// By default, metrics use the global OpenTelemetry MeterProvider. The returned +// value may be reused across multiple caches. Call Unregister to remove its +// OpenTelemetry callbacks and release references to registered sources. +func New(options ...Option) *Metrics { + settings := settings{} + + for _, option := range options { + if option != nil { + option(&settings) + } + } + + meterProvider := settings.meterProvider + if meterProvider == nil { + meterProvider = otel.GetMeterProvider() + } + + return &Metrics{ + meter: meterProvider.Meter( + ScopeName, + metric.WithInstrumentationVersion(Version()), + ), + registrations: make(map[string]metric.Registration), + } +} + +// Register adds one cache metrics source to this OpenTelemetry integration. +// The source name must be unique within Metrics. An empty name is allowed, but +// only one unnamed source may be registered. +func (metrics *Metrics) Register(source pacecache.MetricsSource) error { + if metrics == nil { + return errors.New("paceotel: metrics is nil") + } + + if source == nil { + return errors.New("paceotel: source is nil") + } + + name := source.Name() + + metrics.mu.Lock() + defer metrics.mu.Unlock() + + if metrics.closed { + return errors.New("paceotel: metrics is unregistered") + } + + if _, exists := metrics.registrations[name]; exists { + return fmt.Errorf("paceotel: metrics source %q already registered", name) + } + + if metrics.instruments == nil { + instruments, err := newMetricInstruments(metrics.meter) + if err != nil { + return err + } + + metrics.instruments = &instruments + } + + registration, err := metrics.registerCallback( + source, + newMetricAttributes(name), + ) + if err != nil { + return err + } + + metrics.registrations[name] = registration + + return nil } -func (registration *metricsRegistration) Close() { - if registration == nil || registration.registration == nil { - return +// Unregister removes the OpenTelemetry callbacks associated with Metrics and +// releases references to all successfully unregistered sources. Repeated calls +// are safe and retry callbacks that previously failed to unregister. +func (metrics *Metrics) Unregister() error { + if metrics == nil { + return nil } - if err := registration.registration.Unregister(); err != nil { - otel.Handle( - fmt.Errorf("paceotel: unregister metrics: %w", err), - ) + metrics.mu.Lock() + defer metrics.mu.Unlock() + + metrics.closed = true + metrics.instruments = nil + + var unregisterErr error + + for name, registration := range metrics.registrations { + if err := registration.Unregister(); err != nil { + unregisterErr = errors.Join( + unregisterErr, + fmt.Errorf("source %q: %w", name, err), + ) + continue + } + + delete(metrics.registrations, name) } + + if len(metrics.registrations) == 0 { + metrics.registrations = nil + } + + if unregisterErr != nil { + return fmt.Errorf("paceotel: unregister metrics: %w", unregisterErr) + } + + return nil +} + +func (metrics *Metrics) registerCallback( + source pacecache.MetricsSource, + attributes metricAttributes, +) (metric.Registration, error) { + instruments := *metrics.instruments + + registration, err := metrics.meter.RegisterCallback( + func(_ context.Context, observer metric.Observer) error { + instruments.observe( + observer, + source.Stats(), + attributes, + ) + + return nil + }, + instruments.observables()..., + ) + if err != nil { + return nil, fmt.Errorf("paceotel: register metrics callback: %w", err) + } + + return registration, nil } diff --git a/extra/paceotel/metrics_test.go b/extra/paceotel/metrics_test.go index 0ae7ce0..303a977 100644 --- a/extra/paceotel/metrics_test.go +++ b/extra/paceotel/metrics_test.go @@ -1,14 +1,48 @@ package paceotel import ( + "context" "errors" + "fmt" + "strings" + "sync" "sync/atomic" "testing" + "time" - "go.opentelemetry.io/otel" + "github.com/mkbeh/pacecache" + "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/metric/embedded" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" ) +type metricsSourceStub struct { + name string + stats pacecache.Stats + statsFn func() pacecache.Stats +} + +func (source *metricsSourceStub) Name() string { + if source == nil { + return "" + } + + return source.name +} + +func (source *metricsSourceStub) Stats() pacecache.Stats { + if source == nil { + return pacecache.Stats{} + } + + if source.statsFn != nil { + return source.statsFn() + } + + return source.stats +} + type registrationStub struct { embedded.Registration @@ -22,64 +56,459 @@ func (registration *registrationStub) Unregister() error { return registration.err } -func TestMetricsRegistrationClose(t *testing.T) { - underlying := ®istrationStub{} - registration := &metricsRegistration{ - registration: underlying, +func TestNewIgnoresNilOption(t *testing.T) { + metrics := New(nil) + if metrics == nil { + t.Fatal("New(nil) = nil") } - registration.Close() + if err := metrics.Unregister(); err != nil { + t.Fatalf("Unregister() error = %v", err) + } +} + +func TestMetricsRegisterValidation(t *testing.T) { + t.Run("nil metrics", func(t *testing.T) { + var metrics *Metrics - if got := underlying.calls.Load(); got != 1 { - t.Fatalf("Unregister() calls = %d, want 1", got) + err := metrics.Register(&metricsSourceStub{name: "users"}) + if err == nil || err.Error() != "paceotel: metrics is nil" { + t.Fatalf("Register() error = %v, want metrics is nil", err) + } + }) + + t.Run("nil source", func(t *testing.T) { + metrics, _ := newTestMetrics(t) + + err := metrics.Register(nil) + if err == nil || err.Error() != "paceotel: source is nil" { + t.Fatalf("Register() error = %v, want source is nil", err) + } + }) +} + +func TestMetricsRegisterDuplicateName(t *testing.T) { + metrics, _ := newTestMetrics(t) + + if err := metrics.Register(&metricsSourceStub{name: "users"}); err != nil { + t.Fatalf("Register() error = %v", err) + } + + err := metrics.Register(&metricsSourceStub{name: "users"}) + const want = `paceotel: metrics source "users" already registered` + if err == nil || err.Error() != want { + t.Fatalf("Register() error = %v, want %q", err, want) + } +} + +func TestMetricsRegisterDuplicateUnnamedSource(t *testing.T) { + metrics, _ := newTestMetrics(t) + + if err := metrics.Register(&metricsSourceStub{}); err != nil { + t.Fatalf("Register() error = %v", err) + } + + err := metrics.Register(&metricsSourceStub{}) + const want = `paceotel: metrics source "" already registered` + if err == nil || err.Error() != want { + t.Fatalf("Register() error = %v, want %q", err, want) } } -func TestMetricsRegistrationCloseNilSafe(_ *testing.T) { - var registration *metricsRegistration - registration.Close() +func TestMetricsRegisterAfterUnregister(t *testing.T) { + metrics, _ := newTestMetrics(t) - registration = &metricsRegistration{} - registration.Close() + if err := metrics.Unregister(); err != nil { + t.Fatalf("Unregister() error = %v", err) + } + + err := metrics.Register(&metricsSourceStub{name: "users"}) + if err == nil || err.Error() != "paceotel: metrics is unregistered" { + t.Fatalf("Register() error = %v, want metrics is unregistered", err) + } } -func TestMetricsRegistrationCloseReportsUnregisterError(t *testing.T) { - sentinel := errors.New("unregister failed") - underlying := ®istrationStub{err: sentinel} - registration := &metricsRegistration{ - registration: underlying, +func TestMetricsUnregisterNilSafe(t *testing.T) { + var metrics *Metrics + + if err := metrics.Unregister(); err != nil { + t.Fatalf("Unregister() error = %v", err) } +} - previous := otel.GetErrorHandler() - t.Cleanup(func() { - otel.SetErrorHandler(previous) - }) +func TestMetricsUnregisterRepeated(t *testing.T) { + registration := ®istrationStub{} + metrics := &Metrics{ + registrations: map[string]metric.Registration{ + "users": registration, + }, + } + + if err := metrics.Unregister(); err != nil { + t.Fatalf("first Unregister() error = %v", err) + } + if err := metrics.Unregister(); err != nil { + t.Fatalf("second Unregister() error = %v", err) + } + + if got := registration.calls.Load(); got != 1 { + t.Fatalf("Registration.Unregister() calls = %d, want 1", got) + } +} + +func TestMetricsUnregisterRetriesFailures(t *testing.T) { + firstErr := errors.New("first unregister failed") + secondErr := errors.New("second unregister failed") + + firstFailed := ®istrationStub{err: firstErr} + secondFailed := ®istrationStub{err: secondErr} + successful := ®istrationStub{} + + metrics := &Metrics{ + registrations: map[string]metric.Registration{ + "first": firstFailed, + "second": secondFailed, + "successful": successful, + }, + } + + err := metrics.Unregister() + if !errors.Is(err, firstErr) { + t.Fatalf("Unregister() error = %v, want wrapped first error", err) + } + if !errors.Is(err, secondErr) { + t.Fatalf("Unregister() error = %v, want wrapped second error", err) + } + if !strings.Contains(err.Error(), `source "first"`) { + t.Fatalf("Unregister() error = %q, want first source name", err) + } + if !strings.Contains(err.Error(), `source "second"`) { + t.Fatalf("Unregister() error = %q, want second source name", err) + } + + if got := firstFailed.calls.Load(); got != 1 { + t.Fatalf("first failed Unregister() calls = %d, want 1", got) + } + if got := secondFailed.calls.Load(); got != 1 { + t.Fatalf("second failed Unregister() calls = %d, want 1", got) + } + if got := successful.calls.Load(); got != 1 { + t.Fatalf("successful Unregister() calls = %d, want 1", got) + } + + registerErr := metrics.Register(&metricsSourceStub{name: "new"}) + if registerErr == nil || registerErr.Error() != "paceotel: metrics is unregistered" { + t.Fatalf( + "Register() after failed Unregister() error = %v, want metrics is unregistered", + registerErr, + ) + } + + firstFailed.err = nil + secondFailed.err = nil + + if err := metrics.Unregister(); err != nil { + t.Fatalf("retry Unregister() error = %v", err) + } + + if got := firstFailed.calls.Load(); got != 2 { + t.Fatalf("first failed Unregister() calls after retry = %d, want 2", got) + } + if got := secondFailed.calls.Load(); got != 2 { + t.Fatalf("second failed Unregister() calls after retry = %d, want 2", got) + } + if got := successful.calls.Load(); got != 1 { + t.Fatalf("successful Unregister() calls after retry = %d, want 1", got) + } + + if err := metrics.Unregister(); err != nil { + t.Fatalf("third Unregister() error = %v", err) + } + if got := firstFailed.calls.Load(); got != 2 { + t.Fatalf("first failed Unregister() calls after third call = %d, want 2", got) + } + if got := secondFailed.calls.Load(); got != 2 { + t.Fatalf("second failed Unregister() calls after third call = %d, want 2", got) + } + if got := successful.calls.Load(); got != 1 { + t.Fatalf("successful Unregister() calls after third call = %d, want 1", got) + } +} + +func TestMetricsConcurrentRegister(t *testing.T) { + metrics, reader := newTestMetrics(t) + + const count = 16 - var handled atomic.Value - otel.SetErrorHandler( - otel.ErrorHandlerFunc( - func(err error) { - handled.Store(err) - }, - ), + start := make(chan struct{}) + errorsCh := make(chan error, count) + + var waitGroup sync.WaitGroup + for index := range count { + waitGroup.Add(1) + + go func() { + defer waitGroup.Done() + <-start + + errorsCh <- metrics.Register( + &metricsSourceStub{name: fmt.Sprintf("cache-%d", index)}, + ) + }() + } + + close(start) + waitGroup.Wait() + close(errorsCh) + + for err := range errorsCh { + if err != nil { + t.Fatalf("Register() error = %v", err) + } + } + + collected := collectMetricsByName(t, collectTestMetrics(t, reader)) + points := int64GaugePoints( + t, + requireMetric(t, collected, wantEntryCountMetricName), ) + if len(points) != count { + t.Fatalf("entry count points = %d, want %d", len(points), count) + } + + for index := range count { + attributes := map[string]string{ + wantCacheNameAttribute: fmt.Sprintf("cache-%d", index), + } + if !hasMetricAttributes(points, attributes) { + t.Fatalf("entry count point with attributes %v not found", attributes) + } + } +} + +func TestMetricsConcurrentDuplicateRegister(t *testing.T) { + metrics, _ := newTestMetrics(t) + + const count = 16 + + start := make(chan struct{}) + errorsCh := make(chan error, count) + + var waitGroup sync.WaitGroup + for range count { + waitGroup.Add(1) + + go func() { + defer waitGroup.Done() + <-start + + errorsCh <- metrics.Register(&metricsSourceStub{name: "users"}) + }() + } + + close(start) + waitGroup.Wait() + close(errorsCh) + + var successful int + var duplicates int + + for err := range errorsCh { + switch { + case err == nil: + successful++ + case err.Error() == `paceotel: metrics source "users" already registered`: + duplicates++ + default: + t.Fatalf("Register() error = %v", err) + } + } + + if successful != 1 { + t.Fatalf("successful registrations = %d, want 1", successful) + } + if duplicates != count-1 { + t.Fatalf("duplicate registrations = %d, want %d", duplicates, count-1) + } +} + +func TestMetricsConcurrentRegisterWithUnregister(t *testing.T) { + metrics, _ := newTestMetrics(t) + + start := make(chan struct{}) + registerDone := make(chan error, 1) + unregisterDone := make(chan error, 1) + + go func() { + <-start + registerDone <- metrics.Register(&metricsSourceStub{name: "users"}) + }() + + go func() { + <-start + unregisterDone <- metrics.Unregister() + }() + + close(start) + + select { + case registerErr := <-registerDone: + if registerErr != nil && registerErr.Error() != "paceotel: metrics is unregistered" { + t.Fatalf("Register() error = %v", registerErr) + } + case <-time.After(5 * time.Second): + t.Fatal("Register() did not complete") + } + + select { + case err := <-unregisterDone: + if err != nil { + t.Fatalf("Unregister() error = %v", err) + } + case <-time.After(5 * time.Second): + t.Fatal("Unregister() did not complete") + } + + err := metrics.Register(&metricsSourceStub{name: "after"}) + if err == nil || err.Error() != "paceotel: metrics is unregistered" { + t.Fatalf("Register() after concurrent Unregister() error = %v, want metrics is unregistered", err) + } +} + +func TestMetricsConcurrentUnregister(t *testing.T) { + registration := ®istrationStub{} + metrics := &Metrics{ + registrations: map[string]metric.Registration{ + "users": registration, + }, + } + + const count = 16 - registration.Close() + start := make(chan struct{}) + errorsCh := make(chan error, count) - if got := underlying.calls.Load(); got != 1 { - t.Fatalf("Unregister() calls = %d, want 1", got) + var waitGroup sync.WaitGroup + for range count { + waitGroup.Add(1) + + go func() { + defer waitGroup.Done() + <-start + + errorsCh <- metrics.Unregister() + }() } - value := handled.Load() - if value == nil { - t.Fatal("OpenTelemetry error handler was not called") + close(start) + waitGroup.Wait() + close(errorsCh) + + for err := range errorsCh { + if err != nil { + t.Fatalf("Unregister() error = %v", err) + } } - err, ok := value.(error) - if !ok { - t.Fatalf("handled value = %T, want error", value) + if got := registration.calls.Load(); got != 1 { + t.Fatalf("Registration.Unregister() calls = %d, want 1", got) } - if !errors.Is(err, sentinel) { - t.Fatalf("handled error = %v, want wrapped sentinel", err) +} + +func TestMetricsCollectConcurrentWithUnregister(t *testing.T) { + metrics, reader := newTestMetrics(t) + + started := make(chan struct{}) + release := make(chan struct{}) + + var startOnce sync.Once + source := &metricsSourceStub{ + name: "users", + statsFn: func() pacecache.Stats { + startOnce.Do(func() { + close(started) + }) + <-release + + return pacecache.Stats{EntryCount: 1} + }, + } + + if err := metrics.Register(source); err != nil { + t.Fatalf("Register() error = %v", err) } + + collectDone := make(chan error, 1) + go func() { + var resourceMetrics metricdata.ResourceMetrics + collectDone <- reader.Collect(context.Background(), &resourceMetrics) + }() + + select { + case <-started: + case <-time.After(5 * time.Second): + t.Fatal("metric callback did not start") + } + + unregisterDone := make(chan error, 1) + go func() { + unregisterDone <- metrics.Unregister() + }() + + close(release) + + select { + case err := <-collectDone: + if err != nil { + t.Fatalf("Collect() error = %v", err) + } + case <-time.After(5 * time.Second): + t.Fatal("Collect() did not complete") + } + + select { + case err := <-unregisterDone: + if err != nil { + t.Fatalf("Unregister() error = %v", err) + } + case <-time.After(5 * time.Second): + t.Fatal("Unregister() did not complete") + } + + var resourceMetrics metricdata.ResourceMetrics + if err := reader.Collect(context.Background(), &resourceMetrics); err != nil { + t.Fatalf("Collect() after Unregister() error = %v", err) + } + + for _, scopeMetrics := range resourceMetrics.ScopeMetrics { + if scopeMetrics.Scope.Name == wantScopeName && len(scopeMetrics.Metrics) != 0 { + t.Fatalf( + "metrics after Unregister() = %d, want 0", + len(scopeMetrics.Metrics), + ) + } + } +} + +func newTestMetrics(t *testing.T) (*Metrics, *sdkmetric.ManualReader) { + t.Helper() + + reader := sdkmetric.NewManualReader() + meterProvider := sdkmetric.NewMeterProvider( + sdkmetric.WithReader(reader), + ) + metrics := New( + WithMeterProvider(meterProvider), + ) + + t.Cleanup(func() { + if err := metrics.Unregister(); err != nil { + t.Errorf("Unregister() error = %v", err) + } + if err := meterProvider.Shutdown(context.Background()); err != nil { + t.Errorf("Shutdown() error = %v", err) + } + }) + + return metrics, reader } diff --git a/extra/paceotel/options.go b/extra/paceotel/options.go index 5aac26c..6f40f24 100644 --- a/extra/paceotel/options.go +++ b/extra/paceotel/options.go @@ -9,24 +9,6 @@ type settings struct { meterProvider metric.MeterProvider } -// New creates an OpenTelemetry metrics implementation. -// -// By default, metrics use the global OpenTelemetry MeterProvider. The returned -// value is immutable and may be reused across multiple caches. -func New(options ...Option) *Metrics { - settings := settings{} - - for _, option := range options { - if option != nil { - option(&settings) - } - } - - return &Metrics{ - meterProvider: settings.meterProvider, - } -} - // WithMeterProvider configures the MeterProvider used for cache metrics. // // The caller owns the provider and is responsible for its lifecycle. diff --git a/extra/paceotel/options_test.go b/extra/paceotel/options_test.go index b636534..f90a4bc 100644 --- a/extra/paceotel/options_test.go +++ b/extra/paceotel/options_test.go @@ -7,48 +7,33 @@ import ( sdkmetric "go.opentelemetry.io/otel/sdk/metric" ) -func TestNew(t *testing.T) { - metrics := New(nil) - if metrics == nil { - t.Fatal("New() = nil") - } - if metrics.meterProvider != nil { - t.Fatalf( - "New() meterProvider = %T, want nil", - metrics.meterProvider, - ) - } -} - func TestWithMeterProvider(t *testing.T) { provider := sdkmetric.NewMeterProvider() t.Cleanup(func() { if err := provider.Shutdown(context.Background()); err != nil { - t.Fatalf("Shutdown() error = %v", err) + t.Errorf("Shutdown() error = %v", err) } }) - metrics := New( - WithMeterProvider(provider), - ) + var settings settings + WithMeterProvider(provider)(&settings) - if metrics.meterProvider != provider { + if settings.meterProvider != provider { t.Fatalf( - "New() meterProvider = %T, want provided MeterProvider", - metrics.meterProvider, + "WithMeterProvider() provider = %T, want provided MeterProvider", + settings.meterProvider, ) } } func TestWithMeterProviderNil(t *testing.T) { - metrics := New( - WithMeterProvider(nil), - ) + var settings settings + WithMeterProvider(nil)(&settings) - if metrics.meterProvider != nil { + if settings.meterProvider != nil { t.Fatalf( - "New() meterProvider = %T, want nil", - metrics.meterProvider, + "WithMeterProvider(nil) provider = %T, want nil", + settings.meterProvider, ) } } diff --git a/extra/paceotel/version.go b/extra/paceotel/version.go new file mode 100644 index 0000000..88cd554 --- /dev/null +++ b/extra/paceotel/version.go @@ -0,0 +1,31 @@ +package paceotel + +import "runtime/debug" + +// ScopeName is the OpenTelemetry instrumentation scope name used by this +// package. +const ScopeName = "github.com/mkbeh/pacecache/extra/paceotel" + +// Version returns the version of the paceotel module from Go build +// information. +// +// It returns "unknown" when build information is unavailable or does not +// contain this module. +func Version() string { + info, ok := debug.ReadBuildInfo() + if !ok { + return "unknown" + } + + if info.Main.Path == ScopeName { + return info.Main.Version + } + + for _, dep := range info.Deps { + if dep.Path == ScopeName { + return dep.Version + } + } + + return "unknown" +} diff --git a/load_test.go b/load_test.go index e93138a..e100038 100644 --- a/load_test.go +++ b/load_test.go @@ -10,7 +10,7 @@ import ( ) func TestNewWithDefaultLoaderRejectsNilLoader(t *testing.T) { - cache, err := NewWithDefaultLoader[string, int]("users", nil) + cache, err := NewWithDefaultLoader[string, int](nil) if cache != nil { t.Fatal("cache must be nil for nil default loader") } @@ -24,7 +24,6 @@ func TestGetOrLoadUsesDefaultLoader(t *testing.T) { var loadedKey string cache, err := NewWithDefaultLoader[string, int]( - "users", func(_ context.Context, key string) (int, bool, error) { calls.Add(1) loadedKey = key @@ -53,7 +52,7 @@ func TestGetOrLoadUsesDefaultLoader(t *testing.T) { } func TestGetOrLoadWithoutDefaultLoaderReturnsErrNoLoaderOnMiss(t *testing.T) { - cache := mustNewCache[int](t, "users") + cache := mustNewCache[int](t) cache.Set("cached", 7, NoExpiration) value, found, err := cache.GetOrLoad(context.Background(), "cached") @@ -72,7 +71,6 @@ func TestGetOrLoadWithOverridesDefaultLoader(t *testing.T) { var overrideCalls atomic.Int64 cache, err := NewWithDefaultLoader[string, int]( - "users", func(context.Context, string) (int, bool, error) { defaultCalls.Add(1) @@ -108,7 +106,6 @@ func TestGetOrLoadWithOverridesDefaultLoader(t *testing.T) { func TestGetOrLoadEntryUsesDefaultLoader(t *testing.T) { cache, err := NewWithDefaultLoader[string, int]( - "users", func(context.Context, string) (int, bool, error) { return 42, true, nil }, @@ -132,7 +129,6 @@ func TestDefaultAndExplicitLoadersShareOneWave(t *testing.T) { var overrideCalls atomic.Int64 cache, err := NewWithDefaultLoader[string, int]( - "users", func(context.Context, string) (int, bool, error) { defaultCalls.Add(1) close(started) @@ -194,7 +190,7 @@ func TestDefaultAndExplicitLoadersShareOneWave(t *testing.T) { } func TestGetOrLoadCachesPositiveResult(t *testing.T) { - cache := mustNewCache[int](t, "users") + cache := mustNewCache[int](t) var calls atomic.Int64 loader := func(context.Context, string) (int, bool, error) { @@ -218,7 +214,7 @@ func TestGetOrLoadCachesPositiveResult(t *testing.T) { } func TestGetOrLoadNotFoundIsNotCached(t *testing.T) { - cache := mustNewCache[int](t, "users", WithMaxEntries(8), WithSegmentCount(1)) + cache := mustNewCache[int](t, WithMaxEntries(8), WithSegmentCount(1)) var calls atomic.Int64 loader := func(context.Context, string) (int, bool, error) { @@ -248,7 +244,6 @@ func TestGetOrLoadNotFoundIsNotCached(t *testing.T) { func TestGetOrLoadEntryPositiveAndNotFound(t *testing.T) { cache := mustNewCache[int]( t, - "users", WithMaxEntries(8), WithSegmentCount(1), WithTTL(time.Minute), @@ -277,7 +272,7 @@ func TestGetOrLoadEntryPositiveAndNotFound(t *testing.T) { } func TestGetOrLoadErrorsAreNotCached(t *testing.T) { - cache := mustNewCache[int](t, "users") + cache := mustNewCache[int](t) sentinel := errors.New("load failed") var calls atomic.Int64 @@ -302,7 +297,7 @@ func TestGetOrLoadErrorsAreNotCached(t *testing.T) { } func TestGetOrLoadLoaderPanicPropagatesToCallerAndDoesNotPoisonKey(t *testing.T) { - cache := mustNewCache[int](t, "users") + cache := mustNewCache[int](t) wantErr := errors.New("boom") var recovered any @@ -344,7 +339,7 @@ func TestGetOrLoadLoaderPanicPropagatesToCallerAndDoesNotPoisonKey(t *testing.T) } func TestGetOrLoadValidatesContextAndLoader(t *testing.T) { - cache := mustNewCache[int](t, "users") + cache := mustNewCache[int](t) if _, _, err := cache.GetOrLoadFunc(nil, "key", func(context.Context, string) (int, bool, error) { return 1, true, nil @@ -364,7 +359,7 @@ func TestGetOrLoadValidatesContextAndLoader(t *testing.T) { } func TestGetOrLoadCanceledMissDoesNotStartLoader(t *testing.T) { - cache := mustNewCache[int](t, "users") + cache := mustNewCache[int](t) ctx, cancel := context.WithCancel(context.Background()) cancel() @@ -382,7 +377,7 @@ func TestGetOrLoadCanceledMissDoesNotStartLoader(t *testing.T) { } func TestGetOrLoadCachedHitIgnoresCanceledContext(t *testing.T) { - cache := mustNewCache[int](t, "users") + cache := mustNewCache[int](t) cache.Set("key", 7, NoExpiration) ctx, cancel := context.WithCancel(context.Background()) @@ -398,7 +393,7 @@ func TestGetOrLoadCachedHitIgnoresCanceledContext(t *testing.T) { } func TestGetOrLoadCoalescesConcurrentMisses(t *testing.T) { - cache := mustNewCache[int](t, "users", WithMaxEntries(32), WithSegmentCount(1)) + cache := mustNewCache[int](t, WithMaxEntries(32), WithSegmentCount(1)) started := make(chan struct{}) release := make(chan struct{}) @@ -474,7 +469,7 @@ func TestGetOrLoadCoalescesConcurrentMisses(t *testing.T) { } func TestGetOrLoadWaiterCanCancelWithoutCancelingSharedLoad(t *testing.T) { - cache := mustNewCache[int](t, "users", WithMaxEntries(8), WithSegmentCount(1)) + cache := mustNewCache[int](t, WithMaxEntries(8), WithSegmentCount(1)) started := make(chan struct{}) release := make(chan struct{}) @@ -524,7 +519,6 @@ func TestGetOrLoadWaiterCanCancelWithoutCancelingSharedLoad(t *testing.T) { func TestGetOrLoadAndGetOrLoadEntryShareOneWave(t *testing.T) { cache := mustNewCache[int]( t, - "users", WithMaxEntries(8), WithSegmentCount(1), WithTTL(time.Minute), @@ -607,7 +601,7 @@ func TestGetOrLoadAndGetOrLoadEntryShareOneWave(t *testing.T) { } func TestGetOrLoadUsesGenericKeyIdentity(t *testing.T) { - cache, err := New[testCompositeKey, int]("users", WithMaxEntries(8), WithSegmentCount(2)) + cache, err := New[testCompositeKey, int](WithMaxEntries(8), WithSegmentCount(2)) if err != nil { t.Fatalf("New() error = %v", err) } @@ -906,7 +900,6 @@ func newPublicationTestCache(t *testing.T) *Cache[string, string] { t.Helper() cache, err := New[string, string]( - "publication-test", WithMaxEntries(64), WithSegmentCount(1), ) diff --git a/lookup_test.go b/lookup_test.go index 8e35d23..0f5b323 100644 --- a/lookup_test.go +++ b/lookup_test.go @@ -8,7 +8,6 @@ import ( func TestCacheExists(t *testing.T) { cache := mustNewCache[int]( t, - "users", WithMaxEntries(8), WithSegmentCount(1), ) @@ -32,7 +31,6 @@ func TestCacheExists(t *testing.T) { func TestCacheRefreshTTL(t *testing.T) { cache := mustNewCache[int]( t, - "users", WithMaxEntries(8), WithSegmentCount(1), WithTTL(time.Minute), @@ -92,7 +90,7 @@ func TestCacheExistsAndRefreshTTLRemoveExpiredEntries(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - cache := mustNewCache[int](t, "users", WithMaxEntries(2), WithSegmentCount(1)) + cache := mustNewCache[int](t, WithMaxEntries(2), WithSegmentCount(1)) stats := cache.stats.segment(0) deadline := cache.store.now() @@ -125,7 +123,7 @@ func TestCacheExistsAndRefreshTTLRemoveExpiredEntries(t *testing.T) { } func TestCacheExistsAndRefreshTTLDoNotUpdateLRU(t *testing.T) { - cache := mustNewCache[int](t, "users", WithMaxEntries(2), WithSegmentCount(1)) + cache := mustNewCache[int](t, WithMaxEntries(2), WithSegmentCount(1)) cache.Set("a", 1, time.Minute) cache.Set("b", 2, time.Minute) @@ -149,7 +147,7 @@ func TestCacheExistsAndRefreshTTLDoNotUpdateLRU(t *testing.T) { } func TestCacheSetGetAndLRU(t *testing.T) { - cache := mustNewCache[int](t, "users", WithMaxEntries(2), WithSegmentCount(1)) + cache := mustNewCache[int](t, WithMaxEntries(2), WithSegmentCount(1)) if value, found := cache.Get("missing"); value != 0 || found { t.Fatalf("Get(missing) = (%d, %t), want (0, false)", value, found) @@ -182,7 +180,6 @@ func TestCacheSetGetAndLRU(t *testing.T) { func TestCacheGetEntry(t *testing.T) { cache := mustNewCache[int]( t, - "users", WithMaxEntries(4), WithSegmentCount(1), WithTTL(time.Minute), diff --git a/metrics.go b/metrics.go index bb96af3..0a26e27 100644 --- a/metrics.go +++ b/metrics.go @@ -1,38 +1,32 @@ package pacecache -// StatsProvider exposes one cache's identity and statistics to a metrics -// implementation. -type StatsProvider interface { +// MetricsSource exposes cache identity and statistics to a metrics implementation. +type MetricsSource interface { Name() string Stats() Stats } -// Metrics registers metrics for a Cache. +// Metrics registers cache statistics with a metrics implementation. // -// Implementations must be safe to reuse across multiple caches. RegisterCache -// may be called concurrently. +// Implementations must be safe to reuse across multiple caches. Register may +// be called concurrently. // -// If RegisterCache returns an error, the implementation must release any -// resources created during the registration attempt. +// If Register returns an error, the implementation must release any resources +// created during the registration attempt. type Metrics interface { - RegisterCache(cache StatsProvider) (MetricsRegistration, error) + Register(source MetricsSource) error } -// MetricsRegistration owns a metrics registration associated with one Cache. -// Close is called once when the Cache is closed. -type MetricsRegistration interface { - Close() -} - -// cacheStatsProvider exposes only the capabilities required by Metrics. -type cacheStatsProvider[K comparable, V any] struct { +// metricsSource exposes only the capabilities required by Metrics. +type metricsSource[K comparable, V any] struct { + name string cache *Cache[K, V] } -func (provider cacheStatsProvider[K, V]) Name() string { - return provider.cache.Name() +func (source metricsSource[K, V]) Name() string { + return source.name } -func (provider cacheStatsProvider[K, V]) Stats() Stats { - return provider.cache.Stats() +func (source metricsSource[K, V]) Stats() Stats { + return source.cache.Stats() } diff --git a/metrics_test.go b/metrics_test.go index 98c9eb4..295d13f 100644 --- a/metrics_test.go +++ b/metrics_test.go @@ -2,98 +2,185 @@ package pacecache import ( "errors" - "sync/atomic" + "slices" + "sync" "testing" ) type testMetrics struct { - registerCalls atomic.Int64 - provider StatsProvider - registration MetricsRegistration + mu sync.Mutex + + registerCalls int + sources []MetricsSource err error - providerClose bool } -func (metrics *testMetrics) RegisterCache(provider StatsProvider) (MetricsRegistration, error) { - metrics.registerCalls.Add(1) - metrics.provider = provider - _, metrics.providerClose = provider.(interface{ Close() }) - return metrics.registration, metrics.err -} +func (metrics *testMetrics) Register(source MetricsSource) error { + metrics.mu.Lock() + defer metrics.mu.Unlock() + + metrics.registerCalls++ + + if metrics.err != nil { + return metrics.err + } + + metrics.sources = append(metrics.sources, source) -type testMetricsRegistration struct { - closeCalls atomic.Int64 + return nil } -func (registration *testMetricsRegistration) Close() { - registration.closeCalls.Add(1) +func (metrics *testMetrics) snapshot() (int, []MetricsSource) { + metrics.mu.Lock() + defer metrics.mu.Unlock() + + return metrics.registerCalls, slices.Clone(metrics.sources) } -func TestMetricsRegistrationLifecycle(t *testing.T) { - registration := &testMetricsRegistration{} - metrics := &testMetrics{registration: registration} +func TestMetricsRegistersSource(t *testing.T) { + metrics := &testMetrics{} - cache, err := New[string, int]("users", WithMetrics(metrics)) + cache, err := New[string, int]( + WithName("users"), + WithMetrics(metrics), + ) if err != nil { t.Fatalf("New() error = %v", err) } - if metrics.registerCalls.Load() != 1 { - t.Fatalf("RegisterCache calls = %d, want 1", metrics.registerCalls.Load()) + registerCalls, sources := metrics.snapshot() + if registerCalls != 1 { + t.Fatalf("Register() calls = %d, want 1", registerCalls) } - if metrics.provider == nil { - t.Fatal("metrics provider is nil") + if len(sources) != 1 { + t.Fatalf("registered sources = %d, want 1", len(sources)) } - if metrics.provider.Name() != "users" { - t.Fatalf("provider name = %q, want users", metrics.provider.Name()) + + source := sources[0] + if source.Name() != "users" { + t.Fatalf("source name = %q, want users", source.Name()) } - if metrics.providerClose { - t.Fatal("metrics provider unexpectedly exposes Cache.Close") + if _, ok := source.(interface{ Close() }); ok { + t.Fatal("metrics source unexpectedly exposes Cache.Close") } cache.Set("a", 1, NoExpiration) - if got := metrics.provider.Stats().EntryCount; got != 1 { - t.Fatalf("provider Stats().EntryCount = %d, want 1", got) + if got := source.Stats().EntryCount; got != 1 { + t.Fatalf("source Stats().EntryCount = %d, want 1", got) + } +} + +func TestMetricsRegistersMultipleCaches(t *testing.T) { + metrics := &testMetrics{} + + users, err := New[string, int]( + WithName("users"), + WithMetrics(metrics), + ) + if err != nil { + t.Fatalf("New(users) error = %v", err) + } + + sessions, err := New[string, int]( + WithName("sessions"), + WithMetrics(metrics), + ) + if err != nil { + t.Fatalf("New(sessions) error = %v", err) + } + + users.Set("a", 1, NoExpiration) + sessions.Set("a", 1, NoExpiration) + sessions.Set("b", 2, NoExpiration) + + registerCalls, sources := metrics.snapshot() + if registerCalls != 2 { + t.Fatalf("Register() calls = %d, want 2", registerCalls) + } + if len(sources) != 2 { + t.Fatalf("registered sources = %d, want 2", len(sources)) + } + + byName := make(map[string]MetricsSource, len(sources)) + for _, source := range sources { + if _, ok := source.(interface{ Close() }); ok { + t.Fatalf("metrics source %q unexpectedly exposes Cache.Close", source.Name()) + } + + if _, exists := byName[source.Name()]; exists { + t.Fatalf("metrics source %q registered more than once", source.Name()) + } + + byName[source.Name()] = source + } + + usersSource, ok := byName["users"] + if !ok { + t.Fatal("users metrics source not registered") + } + if got := usersSource.Stats().EntryCount; got != 1 { + t.Fatalf("users Stats().EntryCount = %d, want 1", got) } - cache.Close() - cache.Close() - if registration.closeCalls.Load() != 1 { - t.Fatalf("registration Close calls = %d, want 1", registration.closeCalls.Load()) + sessionsSource, ok := byName["sessions"] + if !ok { + t.Fatal("sessions metrics source not registered") + } + if got := sessionsSource.Stats().EntryCount; got != 2 { + t.Fatalf("sessions Stats().EntryCount = %d, want 2", got) } } -func TestMetricsRegistrationError(t *testing.T) { +func TestMetricsRegistersUnnamedSource(t *testing.T) { + metrics := &testMetrics{} + + if _, err := New[string, int](WithMetrics(metrics)); err != nil { + t.Fatalf("New() error = %v", err) + } + + _, sources := metrics.snapshot() + if len(sources) != 1 { + t.Fatalf("registered sources = %d, want 1", len(sources)) + } + if sources[0].Name() != "" { + t.Fatalf("source name = %q, want empty", sources[0].Name()) + } +} + +func TestMetricsRegisterError(t *testing.T) { sentinel := errors.New("register failed") metrics := &testMetrics{err: sentinel} - cache, err := New[string, int]("users", WithMetrics(metrics)) + cache, err := New[string, int]( + WithName("users"), + WithMetrics(metrics), + ) if cache != nil { t.Fatal("cache must be nil when metrics registration fails") } if !errors.Is(err, sentinel) { t.Fatalf("error = %v, want wrapped sentinel", err) } - if metrics.registerCalls.Load() != 1 { - t.Fatalf("RegisterCache calls = %d, want 1", metrics.registerCalls.Load()) + + registerCalls, sources := metrics.snapshot() + if registerCalls != 1 { + t.Fatalf("Register() calls = %d, want 1", registerCalls) + } + if len(sources) != 0 { + t.Fatalf("registered sources = %d, want 0", len(sources)) } } -func TestMetricsMayReturnNilRegistration(t *testing.T) { - metrics := &testMetrics{} - cache, err := New[string, int]("users", WithMetrics(metrics)) +func TestMetricsNilIsNoop(t *testing.T) { + cache, err := New[string, int](WithMetrics(nil)) if err != nil { t.Fatalf("New() error = %v", err) } - cache.Close() -} -func TestRegisterMetricsNilIsNoop(t *testing.T) { - cache := &Cache[string, int]{} - if err := cache.registerMetrics(nil); err != nil { - t.Fatalf("registerMetrics(nil) error = %v", err) - } - if cache.metrics != nil { - t.Fatal("metrics registration must remain nil") + cache.Set("a", 1, NoExpiration) + + value, found := cache.Get("a") + if !found || value != 1 { + t.Fatalf("Get(a) = %d, %t, want 1, true", value, found) } } diff --git a/options.go b/options.go index 47b99ae..737b9b7 100644 --- a/options.go +++ b/options.go @@ -14,9 +14,9 @@ const ( ) // Option configures a Cache created by New or NewWithDefaultLoader. -type Option func(*cacheSettings) error +type Option func(*settings) error -type cacheSettings struct { +type settings struct { name string maxEntries int @@ -33,9 +33,8 @@ type cacheSettings struct { metrics Metrics } -func newCacheSettings(name string, options ...Option) (*cacheSettings, error) { - settings := defaultCacheSettings() - settings.name = name +func newSettings(options ...Option) (*settings, error) { + settings := defaultSettings() for index, option := range options { if option == nil { @@ -54,8 +53,8 @@ func newCacheSettings(name string, options ...Option) (*cacheSettings, error) { return settings, nil } -func defaultCacheSettings() *cacheSettings { - return &cacheSettings{ +func defaultSettings() *settings { + return &settings{ maxEntries: defaultMaxEntries, segmentCount: defaultStorageSegmentCount, ttl: defaultTTL, @@ -64,6 +63,18 @@ func defaultCacheSettings() *cacheSettings { } } +// WithName configures an optional logical cache name. +// +// Metrics implementations may use the name to distinguish cache instances. +// An empty name leaves the cache unnamed. +func WithName(name string) Option { + return func(settings *settings) error { + settings.name = name + + return nil + } +} + // WithMaxEntries configures the total cache entry budget. // // With one segment, the full budget is shared by the cache. When multiple @@ -71,7 +82,7 @@ func defaultCacheSettings() *cacheSettings { // capacity utilization may be slightly lower because each segment enforces its // own local budget. func WithMaxEntries(maxEntries int) Option { - return func(settings *cacheSettings) error { + return func(settings *settings) error { if maxEntries <= 0 { return errors.New("max entries must be positive") } @@ -89,7 +100,7 @@ func WithMaxEntries(maxEntries int) Option { // segment has its own entry budget. Benchmark segment counts against the // application's actual workload. func WithSegmentCount(count int) Option { - return func(settings *cacheSettings) error { + return func(settings *settings) error { if count <= 0 { return errors.New("segment count must be positive") } @@ -105,7 +116,7 @@ func WithSegmentCount(count int) Option { // A positive TTL enables time-based expiration. NoExpiration disables // time-based expiration for entries using the default expiration. func WithTTL(ttl time.Duration) Option { - return func(settings *cacheSettings) error { + return func(settings *settings) error { if ttl <= 0 && ttl != NoExpiration { return errors.New("ttl must be positive or NoExpiration") } @@ -123,7 +134,7 @@ func WithTTL(ttl time.Duration) Option { // expiration, the resulting effective TTL is reused on every refresh instead of // selecting another jitter value. Zero disables jitter. func WithJitter(jitter time.Duration) Option { - return func(settings *cacheSettings) error { + return func(settings *settings) error { if jitter < 0 { return errors.New("jitter must not be negative") } @@ -143,7 +154,7 @@ func WithJitter(jitter time.Duration) Option { // jitter is selected once when the entry is stored and reused by subsequent // refreshes. Entries using NoExpiration are not refreshed. func WithSlidingExpiration() Option { - return func(settings *cacheSettings) error { + return func(settings *settings) error { settings.slidingExpiration = true return nil @@ -155,8 +166,8 @@ func WithSlidingExpiration() Option { // // The interval controls regular background cleanup wakeups. While expired // backlog remains, the cleaner may schedule bounded continuation work sooner. -// The interval does not affect logical TTL precision or the internal -// expiration bucket resolution. +// The interval does not affect logical TTL precision or the internal expiration +// bucket resolution. // // Background cleanup is disabled by default. This is the only cleanup option // that starts a background worker; cleanup batch size and entry budget only @@ -164,7 +175,7 @@ func WithSlidingExpiration() Option { // always available without this option. When background cleanup is enabled, // Close must be called to stop the cleaner goroutine. func WithCleanupInterval(interval time.Duration) Option { - return func(settings *cacheSettings) error { + return func(settings *settings) error { if interval <= 0 { return errors.New("cleanup interval must be positive") } @@ -183,7 +194,7 @@ func WithCleanupInterval(interval time.Duration) Option { // Values larger than a segment or the remaining cleanup budget are safe and // are naturally limited by the available work. The default is 256. func WithCleanupBatchSize(size int) Option { - return func(settings *cacheSettings) error { + return func(settings *settings) error { if size <= 0 { return errors.New("cleanup batch size must be positive") } @@ -204,7 +215,7 @@ func WithCleanupBatchSize(size int) Option { // all entries due at the start of the call are drained. Values larger than the // cache size are safe. The default is 16384. func WithCleanupEntryBudget(entries int) Option { - return func(settings *cacheSettings) error { + return func(settings *settings) error { if entries <= 0 { return errors.New("cleanup entry budget must be positive") } @@ -217,21 +228,17 @@ func WithCleanupEntryBudget(entries int) Option { // WithMetrics configures optional cache metrics. // -// The Metrics implementation may be reused by multiple caches. Its -// registration is released when Cache.Close is called. +// The Metrics implementation may be reused by multiple caches and owns the +// lifecycle of its registrations. func WithMetrics(metrics Metrics) Option { - return func(settings *cacheSettings) error { + return func(settings *settings) error { settings.metrics = metrics return nil } } -func (settings *cacheSettings) validate() error { - if settings.name == "" { - return errors.New("cache name must not be empty") - } - +func (settings *settings) validate() error { if settings.ttl > 0 && settings.ttl > maxDuration-settings.jitter { return errors.New("ttl plus jitter exceeds maximum duration") } diff --git a/options_test.go b/options_test.go index 999de5d..27a7990 100644 --- a/options_test.go +++ b/options_test.go @@ -2,14 +2,16 @@ package pacecache import ( "errors" - "strings" "testing" "time" ) -func TestDefaultCacheSettings(t *testing.T) { - settings := defaultCacheSettings() +func TestDefaultSettings(t *testing.T) { + settings := defaultSettings() + if settings.name != "" { + t.Fatalf("name = %q, want empty", settings.name) + } if settings.maxEntries != defaultMaxEntries { t.Fatalf("maxEntries = %d, want %d", settings.maxEntries, defaultMaxEntries) } @@ -36,11 +38,11 @@ func TestDefaultCacheSettings(t *testing.T) { } } -func TestNewCacheSettingsAppliesOptions(t *testing.T) { +func TestNewSettingsAppliesOptions(t *testing.T) { metrics := &testMetrics{} - settings, err := newCacheSettings( - "users", + got, err := newSettings( + WithName("users"), WithMaxEntries(128), WithSegmentCount(8), WithTTL(2*time.Minute), @@ -52,93 +54,94 @@ func TestNewCacheSettingsAppliesOptions(t *testing.T) { WithMetrics(metrics), ) if err != nil { - t.Fatalf("newCacheSettings() error = %v", err) + t.Fatalf("newSettings() error = %v", err) + } + + want := settings{ + name: "users", + maxEntries: 128, + segmentCount: 8, + ttl: 2 * time.Minute, + jitter: 15 * time.Second, + slidingExpiration: true, + cleanupInterval: time.Second, + cleanupBatchSize: 1024, + cleanupEntryBudget: 64 * 1024, + metrics: metrics, } - if settings.name != "users" || - settings.maxEntries != 128 || - settings.segmentCount != 8 || - settings.ttl != 2*time.Minute || - settings.jitter != 15*time.Second || - !settings.slidingExpiration || - settings.cleanupInterval != time.Second || - settings.cleanupBatchSize != 1024 || - settings.cleanupEntryBudget != 64*1024 || - settings.metrics != metrics { - t.Fatalf("unexpected settings: %+v", settings) + if *got != want { + t.Fatalf("newSettings() = %+v, want %+v", *got, want) } } -func TestNewCacheSettingsRejectsNilOption(t *testing.T) { - _, err := newCacheSettings("users", nil) - if err == nil || !strings.Contains(err.Error(), "option 0 is nil") { - t.Fatalf("error = %v, want nil-option error", err) +func TestNewSettingsRejectsNilOption(t *testing.T) { + _, err := newSettings(nil) + const want = "option 0 is nil" + if err == nil || err.Error() != want { + t.Fatalf("error = %v, want %q", err, want) } } -func TestNewCacheSettingsWrapsOptionError(t *testing.T) { +func TestNewSettingsWrapsOptionError(t *testing.T) { sentinel := errors.New("sentinel") - option := func(*cacheSettings) error { return sentinel } + option := func(*settings) error { return sentinel } - _, err := newCacheSettings("users", option) + _, err := newSettings(option) if !errors.Is(err, sentinel) { t.Fatalf("error = %v, want wrapped sentinel", err) } } -func TestCacheSettingsValidation(t *testing.T) { +func TestSettingsValidation(t *testing.T) { tests := []struct { name string - cache string options []Option want string }{ - {name: "empty name", cache: "", want: "cache name must not be empty"}, - {name: "max entries zero", cache: "users", options: []Option{WithMaxEntries(0)}, want: "max entries must be positive"}, - {name: "max entries negative", cache: "users", options: []Option{WithMaxEntries(-1)}, want: "max entries must be positive"}, - {name: "segment count zero", cache: "users", options: []Option{WithSegmentCount(0)}, want: "segment count must be positive"}, - {name: "segment count negative", cache: "users", options: []Option{WithSegmentCount(-1)}, want: "segment count must be positive"}, - {name: "ttl zero", cache: "users", options: []Option{WithTTL(0)}, want: "ttl must be positive or NoExpiration"}, - {name: "ttl invalid negative", cache: "users", options: []Option{WithTTL(-2)}, want: "ttl must be positive or NoExpiration"}, - {name: "negative jitter", cache: "users", options: []Option{WithJitter(-1)}, want: "jitter must not be negative"}, - {name: "cleanup interval zero", cache: "users", options: []Option{WithCleanupInterval(0)}, want: "cleanup interval must be positive"}, - {name: "cleanup interval negative", cache: "users", options: []Option{WithCleanupInterval(-1)}, want: "cleanup interval must be positive"}, - {name: "cleanup batch size zero", cache: "users", options: []Option{WithCleanupBatchSize(0)}, want: "cleanup batch size must be positive"}, - {name: "cleanup batch size negative", cache: "users", options: []Option{WithCleanupBatchSize(-1)}, want: "cleanup batch size must be positive"}, - {name: "cleanup entry budget zero", cache: "users", options: []Option{WithCleanupEntryBudget(0)}, want: "cleanup entry budget must be positive"}, - {name: "cleanup entry budget negative", cache: "users", options: []Option{WithCleanupEntryBudget(-1)}, want: "cleanup entry budget must be positive"}, - {name: "segments exceed max entries", cache: "users", options: []Option{WithMaxEntries(2), WithSegmentCount(3)}, want: "segment count must not exceed max entries"}, + {name: "max entries zero", options: []Option{WithMaxEntries(0)}, want: "apply option 0: max entries must be positive"}, + {name: "max entries negative", options: []Option{WithMaxEntries(-1)}, want: "apply option 0: max entries must be positive"}, + {name: "segment count zero", options: []Option{WithSegmentCount(0)}, want: "apply option 0: segment count must be positive"}, + {name: "segment count negative", options: []Option{WithSegmentCount(-1)}, want: "apply option 0: segment count must be positive"}, + {name: "ttl zero", options: []Option{WithTTL(0)}, want: "apply option 0: ttl must be positive or NoExpiration"}, + {name: "ttl invalid negative", options: []Option{WithTTL(-2)}, want: "apply option 0: ttl must be positive or NoExpiration"}, + {name: "negative jitter", options: []Option{WithJitter(-1)}, want: "apply option 0: jitter must not be negative"}, + {name: "cleanup interval zero", options: []Option{WithCleanupInterval(0)}, want: "apply option 0: cleanup interval must be positive"}, + {name: "cleanup interval negative", options: []Option{WithCleanupInterval(-1)}, want: "apply option 0: cleanup interval must be positive"}, + {name: "cleanup batch size zero", options: []Option{WithCleanupBatchSize(0)}, want: "apply option 0: cleanup batch size must be positive"}, + {name: "cleanup batch size negative", options: []Option{WithCleanupBatchSize(-1)}, want: "apply option 0: cleanup batch size must be positive"}, + {name: "cleanup entry budget zero", options: []Option{WithCleanupEntryBudget(0)}, want: "apply option 0: cleanup entry budget must be positive"}, + {name: "cleanup entry budget negative", options: []Option{WithCleanupEntryBudget(-1)}, want: "apply option 0: cleanup entry budget must be positive"}, + {name: "segments exceed max entries", options: []Option{WithMaxEntries(2), WithSegmentCount(3)}, want: "invalid configuration: segment count must not exceed max entries"}, { - name: "ttl plus jitter overflow", - cache: "users", + name: "ttl plus jitter overflow", options: []Option{ WithTTL(maxDuration), WithJitter(time.Nanosecond), }, - want: "ttl plus jitter exceeds maximum duration", + want: "invalid configuration: ttl plus jitter exceeds maximum duration", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - _, err := newCacheSettings(test.cache, test.options...) - if err == nil || !strings.Contains(err.Error(), test.want) { - t.Fatalf("error = %v, want substring %q", err, test.want) + _, err := newSettings(test.options...) + if err == nil || err.Error() != test.want { + t.Fatalf("error = %v, want %q", err, test.want) } }) } } -func TestCacheSettingsAcceptsIndependentCleanupLimits(t *testing.T) { - settings, err := newCacheSettings( - "users", +func TestSettingsAcceptsIndependentCleanupLimits(t *testing.T) { + settings, err := newSettings( WithMaxEntries(4), WithSegmentCount(1), WithCleanupBatchSize(10_000), WithCleanupEntryBudget(3), ) if err != nil { - t.Fatalf("newCacheSettings() error = %v", err) + t.Fatalf("newSettings() error = %v", err) } if settings.cleanupBatchSize != 10_000 || settings.cleanupEntryBudget != 3 { @@ -146,18 +149,21 @@ func TestCacheSettingsAcceptsIndependentCleanupLimits(t *testing.T) { } } -func TestCacheSettingsAcceptsBoundaryValues(t *testing.T) { - settings, err := newCacheSettings( - "users", +func TestSettingsAcceptsBoundaryValues(t *testing.T) { + settings, err := newSettings( + WithName(""), WithMaxEntries(1), WithTTL(NoExpiration), WithJitter(maxDuration), WithMetrics(nil), ) if err != nil { - t.Fatalf("newCacheSettings() error = %v", err) + t.Fatalf("newSettings() error = %v", err) } + if settings.name != "" { + t.Fatalf("name = %q, want empty", settings.name) + } if settings.ttl != NoExpiration { t.Fatalf("ttl = %v, want NoExpiration", settings.ttl) } diff --git a/set_test.go b/set_test.go index 3e8c551..0002476 100644 --- a/set_test.go +++ b/set_test.go @@ -9,7 +9,7 @@ import ( ) func TestCacheSetOverwritesExistingValue(t *testing.T) { - cache := mustNewCache[int](t, "users", WithMaxEntries(4), WithSegmentCount(1)) + cache := mustNewCache[int](t, WithMaxEntries(4), WithSegmentCount(1)) cache.Set("key", 1, time.Minute) cache.Set("key", 2, NoExpiration) @@ -26,7 +26,6 @@ func TestCacheSetOverwritesExistingValue(t *testing.T) { func TestCacheStoresTTLPolicyForDefaultCustomAndNoExpiration(t *testing.T) { cache := mustNewCache[int]( t, - "users", WithMaxEntries(8), WithSegmentCount(1), WithTTL(10*time.Second), @@ -61,7 +60,6 @@ func TestCacheStoresTTLPolicyForDefaultCustomAndNoExpiration(t *testing.T) { func TestCacheGetOrSet(t *testing.T) { cache := mustNewCache[int]( t, - "users", WithMaxEntries(8), WithSegmentCount(1), WithTTL(10*time.Second), @@ -120,7 +118,6 @@ func TestCacheGetOrSet(t *testing.T) { func TestCacheGetOrSetExpirationPolicies(t *testing.T) { cache := mustNewCache[int]( t, - "users", WithMaxEntries(8), WithSegmentCount(1), WithTTL(10*time.Second), @@ -160,7 +157,7 @@ func TestCacheGetOrSetExpirationPolicies(t *testing.T) { } func TestCacheGetOrSetUsesNoExpirationByDefault(t *testing.T) { - cache := mustNewCache[int](t, "users", WithMaxEntries(8), WithSegmentCount(1)) + cache := mustNewCache[int](t, WithMaxEntries(8), WithSegmentCount(1)) value, found := cache.GetOrSet("key", 42, DefaultExpiration) if found || value != 42 { @@ -177,7 +174,7 @@ func TestCacheGetOrSetUsesNoExpirationByDefault(t *testing.T) { } func TestCacheGetOrSetExpiredEntry(t *testing.T) { - cache := mustNewCache[int](t, "users", WithMaxEntries(8), WithSegmentCount(1)) + cache := mustNewCache[int](t, WithMaxEntries(8), WithSegmentCount(1)) index := cache.store.segmentIndex("key") cache.store.setAt( @@ -211,7 +208,7 @@ func TestCacheGetOrSetExpiredEntry(t *testing.T) { } func TestCacheGetOrSetIsAtomic(t *testing.T) { - cache := mustNewCache[int](t, "users", WithMaxEntries(8), WithSegmentCount(1)) + cache := mustNewCache[int](t, WithMaxEntries(8), WithSegmentCount(1)) const callers = 32 @@ -288,7 +285,7 @@ func TestCacheGetOrSetIsAtomic(t *testing.T) { } func TestCacheGetOrSetSupersedesInflightLoad(t *testing.T) { - cache := mustNewCache[int](t, "users", WithMaxEntries(8), WithSegmentCount(1)) + cache := mustNewCache[int](t, WithMaxEntries(8), WithSegmentCount(1)) started := make(chan struct{}) release := make(chan struct{}) @@ -347,7 +344,6 @@ func TestZeroValueCacheGetOrSet(t *testing.T) { func TestCacheGetOrSetEntry(t *testing.T) { cache := mustNewCache[int]( t, - "users", WithMaxEntries(8), WithSegmentCount(1), WithTTL(10*time.Second), @@ -396,7 +392,7 @@ func TestCacheGetOrSetEntry(t *testing.T) { } func TestCacheGetOrSetEntryNoExpirationMetadata(t *testing.T) { - cache := mustNewCache[int](t, "users", WithMaxEntries(8), WithSegmentCount(1)) + cache := mustNewCache[int](t, WithMaxEntries(8), WithSegmentCount(1)) entry, found := cache.GetOrSetEntry("key", 42, DefaultExpiration) if found || entry.Value() != 42 { @@ -412,7 +408,7 @@ func TestCacheGetOrSetEntryNoExpirationMetadata(t *testing.T) { } func TestCacheGetOrSetEntryIsAtomic(t *testing.T) { - cache := mustNewCache[int](t, "users", WithMaxEntries(8), WithSegmentCount(1)) + cache := mustNewCache[int](t, WithMaxEntries(8), WithSegmentCount(1)) const callers = 32 @@ -487,7 +483,7 @@ func TestCacheGetOrSetEntryIsAtomic(t *testing.T) { } func TestCacheGetOrSetEntrySupersedesInflightLoad(t *testing.T) { - cache := mustNewCache[int](t, "users", WithMaxEntries(8), WithSegmentCount(1)) + cache := mustNewCache[int](t, WithMaxEntries(8), WithSegmentCount(1)) started := make(chan struct{}) release := make(chan struct{}) diff --git a/stats_test.go b/stats_test.go index d497e9a..59a9af7 100644 --- a/stats_test.go +++ b/stats_test.go @@ -11,7 +11,6 @@ func TestStatsSnapshotAggregatesSegments(t *testing.T) { store := newStorageWithExpirationResolution[string, int](4, 2, time.Nanosecond) collector := newStatsCollector(2) cache := &Cache[string, int]{ - name: "users", store: store, states: make([]cacheState[string, int], 2), stats: collector, @@ -46,24 +45,34 @@ func TestStatsSnapshotAggregatesSegments(t *testing.T) { collector.cleanupWorkerPendingCount.Store(16) collector.cleanupWorkerDurationNanos.Store(17) - got := cache.Stats() - if got.EntryCount != 2 || got.MaxEntries != 4 || got.SegmentCount != 2 { - t.Fatalf("state stats = %+v", got) - } - if got.HitCount != 3 || got.MissCount != 7 || got.EvictionCount != 9 || got.ExpirationCount != 11 { - t.Fatalf("storage counters = %+v", got) - } - if got.LoadFoundCount != 13 || got.LoadNotFoundCount != 15 || got.LoadErrorCount != 17 || got.LoadSupersededCount != 19 { - t.Fatalf("load counters = %+v", got) - } - if got.LoadDuration != 2_100*time.Nanosecond { - t.Fatalf("LoadDuration = %v, want 2100ns", got.LoadDuration) - } - if got.SharedCount != 23 || got.DeletedEntryCount != 25 || got.ClearedEntryCount != 13 { - t.Fatalf("atomic counters = %+v", got) + want := Stats{ + EntryCount: 2, + MaxEntries: 4, + SegmentCount: 2, + + HitCount: 3, + MissCount: 7, + EvictionCount: 9, + ExpirationCount: 11, + + LoadFoundCount: 13, + LoadNotFoundCount: 15, + LoadErrorCount: 17, + LoadSupersededCount: 19, + LoadDuration: 2_100 * time.Nanosecond, + SharedCount: 23, + + DeletedEntryCount: 25, + ClearedEntryCount: 13, + + CleanupCount: 14, + CleanupWorkerRunCount: 15, + CleanupWorkerPendingCount: 16, + CleanupWorkerDuration: 17 * time.Nanosecond, } - if got.CleanupCount != 14 || got.CleanupWorkerRunCount != 15 || got.CleanupWorkerPendingCount != 16 || got.CleanupWorkerDuration != 17*time.Nanosecond { - t.Fatalf("cleanup counters = %+v", got) + + if got := cache.Stats(); got != want { + t.Fatalf("Stats() = %+v, want %+v", got, want) } } @@ -130,7 +139,7 @@ func TestStatsRecordHelpers(t *testing.T) { } func TestStatsConcurrentWithCacheOperations(t *testing.T) { - cache := mustNewCache[int](t, "users", WithMaxEntries(128), WithSegmentCount(8)) + cache := mustNewCache[int](t, WithMaxEntries(128), WithSegmentCount(8)) var group sync.WaitGroup for worker := range 8 { From 797a2648eb8afa68fd2d06ace0afff2f5af9d09e Mon Sep 17 00:00:00 2001 From: mkbeh Date: Sat, 19 Sep 2026 07:35:54 +0300 Subject: [PATCH 2/8] refactor: rework cleanup lifecycle --- .../hitratio/internal/policy/policy.go | 4 - .../hitratio/internal/simulator/simulator.go | 1 - benchmarks/performance/memory/main.go | 1 - .../performance/throughput/throughput_test.go | 2 - cache.go | 82 ++++--- cache_test.go | 21 -- cleanup.go | 52 +---- cleanup_test.go | 206 ++++++++++++++---- doc.go | 6 +- load_test.go | 6 - metrics_test.go | 20 +- options.go | 18 +- options_test.go | 11 +- 13 files changed, 262 insertions(+), 168 deletions(-) diff --git a/benchmarks/performance/hitratio/internal/policy/policy.go b/benchmarks/performance/hitratio/internal/policy/policy.go index b7fa6c9..c26c1b1 100644 --- a/benchmarks/performance/hitratio/internal/policy/policy.go +++ b/benchmarks/performance/hitratio/internal/policy/policy.go @@ -59,7 +59,3 @@ func (p *Policy) Ratio() float64 { return 100 * float64(p.hits) / float64(total) } - -func (p *Policy) Close() { - p.cache.Close() -} diff --git a/benchmarks/performance/hitratio/internal/simulator/simulator.go b/benchmarks/performance/hitratio/internal/simulator/simulator.go index 5990855..7c483d9 100644 --- a/benchmarks/performance/hitratio/internal/simulator/simulator.go +++ b/benchmarks/performance/hitratio/internal/simulator/simulator.go @@ -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, diff --git a/benchmarks/performance/memory/main.go b/benchmarks/performance/memory/main.go index 8546d46..e9a20fd 100644 --- a/benchmarks/performance/memory/main.go +++ b/benchmarks/performance/memory/main.go @@ -45,7 +45,6 @@ func main() { if err != nil { log.Fatalf("create cache: %v", err) } - defer cache.Close() for index := range *capacity { key := keys[index] diff --git a/benchmarks/performance/throughput/throughput_test.go b/benchmarks/performance/throughput/throughput_test.go index 09a749b..622b0b1 100644 --- a/benchmarks/performance/throughput/throughput_test.go +++ b/benchmarks/performance/throughput/throughput_test.go @@ -78,7 +78,6 @@ func runThroughputBenchmark( b.Helper() cache := newThroughputCache(b, maxEntries, data) - b.Cleanup(cache.Close) var workers atomic.Uint64 @@ -152,7 +151,6 @@ func newThroughputCache( return cache } - cache.Close() } b.Fatalf( diff --git a/cache.go b/cache.go index 71a1809..32982a5 100644 --- a/cache.go +++ b/cache.go @@ -30,12 +30,14 @@ type Cache[K comparable, V any] struct { states []cacheState[K, V] stats *statsCollector - cleanupPolicy cleanupPolicy - cleanup *cleanupWorker[K, V] - closeOnce sync.Once - ttl time.Duration jitter time.Duration + + cleanupPolicy cleanupPolicy + cleanupInterval time.Duration + + cleanupMu sync.Mutex + cleanup *cleanupWorker[K, V] } // New creates a Cache. @@ -82,11 +84,6 @@ func newCache[K comparable, V any]( settings.slidingExpiration, ) - policy := cleanupPolicy{ - batchSize: settings.cleanupBatchSize, - entryBudget: settings.cleanupEntryBudget, - } - cache := &Cache[K, V]{ loader: loader, @@ -94,43 +91,68 @@ func newCache[K comparable, V any]( states: make([]cacheState[K, V], len(store.segments)), stats: newStatsCollector(len(store.segments)), - cleanupPolicy: policy, - ttl: settings.ttl, jitter: settings.jitter, + + cleanupPolicy: cleanupPolicy{ + batchSize: settings.cleanupBatchSize, + entryBudget: settings.cleanupEntryBudget, + }, + cleanupInterval: settings.cleanupInterval, } if err := cache.registerMetrics(settings.name, settings.metrics); err != nil { return nil, fmt.Errorf("pacecache: register metrics: %w", err) } - if settings.cleanupInterval > 0 { - cache.cleanup = newCleanupWorker( - cache.store, - cache.stats, - cache.cleanupPolicy, - settings.cleanupInterval, - ) - cache.cleanup.start() + return cache, nil +} + +// StartCleanup runs periodic background expiration cleanup until StopCleanup is +// called. StartCleanup blocks for the lifetime of the cleanup loop; callers that +// want background cleanup should start it in a goroutine. If cleanup is already +// running, StartCleanup returns immediately. +func (cache *Cache[K, V]) StartCleanup() { + if !cache.initialized() { + return } - return cache, nil + cache.cleanupMu.Lock() + if cache.cleanup != nil { + cache.cleanupMu.Unlock() + return + } + + worker := newCleanupWorker( + cache.store, + cache.stats, + cache.cleanupPolicy, + cache.cleanupInterval, + ) + cache.cleanup = worker + cache.cleanupMu.Unlock() + + worker.run() } -// Close stops background cleanup and waits for the worker to exit. -// -// Close does not clear or disable the cache. Repeated calls are safe. Close is -// a no-op on a nil Cache. -func (cache *Cache[K, V]) Close() { +// StopCleanup stops a running cleanup loop. It blocks until the cleanup loop +// accepts the stop signal. Repeated calls are safe. StopCleanup is a no-op +// when cleanup is not running or Cache is nil. +func (cache *Cache[K, V]) StopCleanup() { if cache == nil { return } - cache.closeOnce.Do(func() { - if cache.cleanup != nil { - cache.cleanup.close() - } - }) + cache.cleanupMu.Lock() + defer cache.cleanupMu.Unlock() + + worker := cache.cleanup + if worker == nil { + return + } + + worker.stopCh <- struct{}{} + cache.cleanup = nil } func (cache *Cache[K, V]) effectiveTTL(expiration time.Duration) time.Duration { diff --git a/cache_test.go b/cache_test.go index d515802..402a137 100644 --- a/cache_test.go +++ b/cache_test.go @@ -29,8 +29,6 @@ func TestNilCacheIsSafe(t *testing.T) { if _, _, err := cache.GetOrLoadEntry(context.Background(), "key"); !errors.Is(err, ErrNotInitialized) { t.Fatalf("nil Cache.GetOrLoadEntry() error = %v, want ErrNotInitialized", err) } - - cache.Close() } func TestZeroValueCacheIsSafe(t *testing.T) { @@ -53,7 +51,6 @@ func TestZeroValueCacheIsSafe(t *testing.T) { cache.Delete() cache.Delete("key") cache.Clear() - cache.Close() if got := cache.Stats(); got != (Stats{}) { t.Fatalf("zero Cache.Stats() = %+v, want zero Stats", got) @@ -80,20 +77,6 @@ func TestZeroValueCacheLoadReturnsNotInitialized(t *testing.T) { } } -func TestCloseIsIdempotentAndCacheRemainsUsable(t *testing.T) { - cache, err := New[string, int]() - if err != nil { - t.Fatalf("New() error = %v", err) - } - - cache.Close() - cache.Close() - cache.Set("key", 1, NoExpiration) - if value, found := cache.Get("key"); value != 1 || !found { - t.Fatalf("Get after Close = (%d, %t), want usable cache", value, found) - } -} - func TestEffectiveTTLAndDeadlineHelpers(t *testing.T) { cache := &Cache[string, int]{ttl: 10 * time.Second} @@ -165,7 +148,6 @@ func TestCacheSupportsInt64Keys(t *testing.T) { if err != nil { t.Fatalf("New() error = %v", err) } - t.Cleanup(cache.Close) cache.Set(42, "Ada", NoExpiration) @@ -190,7 +172,6 @@ func TestCacheSupportsComparableStructKeys(t *testing.T) { if err != nil { t.Fatalf("New() error = %v", err) } - t.Cleanup(cache.Close) stored := testCompositeKey{TenantID: 7, UserID: 42} equal := testCompositeKey{TenantID: 7, UserID: 42} @@ -260,8 +241,6 @@ func mustNewCache[V any](t *testing.T, options ...Option) *Cache[string, V] { t.Fatalf("New() error = %v", err) } - t.Cleanup(cache.Close) - return cache } diff --git a/cleanup.go b/cleanup.go index 947ce05..7ae0f10 100644 --- a/cleanup.go +++ b/cleanup.go @@ -3,6 +3,7 @@ package pacecache import "time" const ( + defaultCleanupInterval = time.Minute defaultCleanupBatchSize = 256 defaultCleanupEntryBudget = 16 * 1024 @@ -21,8 +22,7 @@ type cleanupWorker[K comparable, V any] struct { policy cleanupPolicy interval time.Duration - stop chan struct{} - done chan struct{} + stopCh chan struct{} // scratchSegments is reusable temporary storage for segment indexes that // still have due entries after a multi-segment cleanup pass. @@ -41,8 +41,7 @@ func newCleanupWorker[K comparable, V any]( stats: stats, policy: policy, interval: interval, - stop: make(chan struct{}), - done: make(chan struct{}), + stopCh: make(chan struct{}), } if len(store.segments) > 1 { @@ -52,23 +51,22 @@ func newCleanupWorker[K comparable, V any]( return worker } -func (worker *cleanupWorker[K, V]) start() { - go worker.run() -} - -func (worker *cleanupWorker[K, V]) close() { - close(worker.stop) - <-worker.done -} - func (worker *cleanupWorker[K, V]) run() { timer := time.NewTimer(worker.interval) defer timer.Stop() - defer close(worker.done) for { select { + case <-worker.stopCh: + return + case <-timer.C: + select { + case <-worker.stopCh: + return + default: + } + cutoff := worker.store.now() pending := worker.cleanupQuantum(cutoff) worker.stats.recordCleanupWorker( @@ -82,9 +80,6 @@ func (worker *cleanupWorker[K, V]) run() { } timer.Reset(next) - - case <-worker.stop: - return } } } @@ -108,10 +103,6 @@ func (worker *cleanupWorker[K, V]) cleanupSingleSegment(cutoff int64) bool { stats := worker.stats.segment(0) for { - if worker.stopped() { - return false - } - if remaining == 0 || cleanupTimeBudgetExceeded(startedAt) { return true } @@ -140,11 +131,6 @@ func (worker *cleanupWorker[K, V]) cleanupSegments(cutoff int64) bool { start := worker.nextSegment for offset := range segmentCount { - if worker.stopped() { - worker.scratchSegments = pending[:0] - return false - } - if remaining == 0 || cleanupTimeBudgetExceeded(startedAt) { worker.nextSegment = (start + offset) % segmentCount worker.scratchSegments = pending[:0] @@ -173,11 +159,6 @@ func (worker *cleanupWorker[K, V]) cleanupSegments(cutoff int64) bool { next := pending[:0] for _, index := range pending { - if worker.stopped() { - worker.scratchSegments = pending[:0] - return false - } - if remaining == 0 || cleanupTimeBudgetExceeded(startedAt) { // Resume from the first pending segment that this quantum did // not get a chance to process. @@ -211,15 +192,6 @@ func (worker *cleanupWorker[K, V]) nextDelay() time.Duration { return min(worker.interval, cleanupNextDelay) } -func (worker *cleanupWorker[K, V]) stopped() bool { - select { - case <-worker.stop: - return true - default: - return false - } -} - func cleanupTimeBudgetExceeded(startedAt time.Time) bool { return time.Since(startedAt) >= cleanupTimeBudget } diff --git a/cleanup_test.go b/cleanup_test.go index 71dac24..66b70d3 100644 --- a/cleanup_test.go +++ b/cleanup_test.go @@ -2,6 +2,7 @@ package pacecache import ( "runtime" + "sync" "testing" "time" ) @@ -37,22 +38,6 @@ func TestCleanupWorkerNextDelay(t *testing.T) { } } -func TestCleanupWorkerStopped(t *testing.T) { - store := newStorageWithExpirationResolution[string, int](1, 1, time.Nanosecond) - worker := newTestCleanupWorker(store, newStatsCollector(1), time.Second) - - if worker.stopped() { - t.Fatal("new worker unexpectedly stopped") - } - close(worker.stop) - if !worker.stopped() { - t.Fatal("closed worker stop channel not observed") - } - if worker.cleanupQuantum(10) { - t.Fatal("cleanup reported pending work after stop") - } -} - func TestCleanupWorkerEmptyStorage(t *testing.T) { store := &storage[string, int]{} worker := newTestCleanupWorker(store, newStatsCollector(0), time.Second) @@ -61,43 +46,145 @@ func TestCleanupWorkerEmptyStorage(t *testing.T) { } } -func TestCleanupWorkerBackgroundRemovesExpiredEntry(t *testing.T) { - store := newStorageWithExpirationResolution[string, int](1, 1, time.Millisecond) - stats := newStatsCollector(1) - now := store.now() - store.setAt(0, "key", 1, 2*time.Millisecond, deadlineAfter(now, 2*time.Millisecond), stats.segment(0)) +func TestNewDoesNotStartCleanup(t *testing.T) { + cache, err := New[string, int](WithCleanupInterval(time.Millisecond)) + if err != nil { + t.Fatalf("New() error = %v", err) + } - worker := newTestCleanupWorker(store, stats, time.Millisecond) - worker.start() - t.Cleanup(worker.close) + if cache.cleanup != nil { + t.Fatal("cleanup unexpectedly running after New") + } +} + +func TestStartCleanupRemovesExpiredEntry(t *testing.T) { + cache, err := New[string, int]( + WithMaxEntries(1), + WithCleanupInterval(time.Millisecond), + ) + if err != nil { + t.Fatalf("New() error = %v", err) + } + + cache.store.enableExpirationIndex(time.Nanosecond) + cache.Set("key", 1, time.Millisecond) + time.Sleep(2 * time.Millisecond) + + done := startTestCleanup(t, cache) eventually(t, 500*time.Millisecond, func() bool { - segment := &store.segments[0] + segment := &cache.store.segments[0] segment.mu.Lock() defer segment.mu.Unlock() + return len(segment.entries) == 0 }) - if stats.segment(0).expirationCount != 1 { - t.Fatalf("expirationCount = %d, want 1", stats.segment(0).expirationCount) + cache.StopCleanup() + waitTestSignal(t, done) + + if cache.stats.segment(0).expirationCount != 1 { + t.Fatalf( + "expirationCount = %d, want 1", + cache.stats.segment(0).expirationCount, + ) } - if stats.cleanupWorkerRunCount.Load() == 0 { + if cache.stats.cleanupWorkerRunCount.Load() == 0 { t.Fatal("cleanup worker run was not recorded") } } -func TestCacheCloseStopsBackgroundCleanup(t *testing.T) { - cache, err := New[string, int](WithCleanupInterval(time.Millisecond)) +func TestStopCleanupWithoutStartIsNoop(t *testing.T) { + cache := mustNewCache[int](t) + + done := make(chan struct{}) + go func() { + cache.StopCleanup() + close(done) + }() + + waitTestSignal(t, done) +} + +func TestStopCleanupStopsRunningWorker(t *testing.T) { + cache, err := New[string, int](WithCleanupInterval(time.Hour)) if err != nil { t.Fatalf("New() error = %v", err) } - if cache.cleanup == nil { - t.Fatal("cleanup worker is nil") + + done := startTestCleanup(t, cache) + + cache.StopCleanup() + waitTestSignal(t, done) + + cache.StopCleanup() +} + +func TestStopCleanupConcurrent(t *testing.T) { + cache, err := New[string, int](WithCleanupInterval(time.Hour)) + if err != nil { + t.Fatalf("New() error = %v", err) + } + + done := startTestCleanup(t, cache) + + const callers = 16 + var group sync.WaitGroup + for range callers { + group.Add(1) + go func() { + defer group.Done() + cache.StopCleanup() + }() + } + + waitTestGroup(t, &group) + waitTestSignal(t, done) +} + +func TestStartCleanupCanRestart(t *testing.T) { + cache, err := New[string, int](WithCleanupInterval(time.Hour)) + if err != nil { + t.Fatalf("New() error = %v", err) + } + + firstDone := startTestCleanup(t, cache) + cache.StopCleanup() + waitTestSignal(t, firstDone) + + secondDone := startTestCleanup(t, cache) + cache.StopCleanup() + waitTestSignal(t, secondDone) +} + +func TestStartCleanupReturnsWhenAlreadyRunning(t *testing.T) { + cache, err := New[string, int](WithCleanupInterval(time.Hour)) + if err != nil { + t.Fatalf("New() error = %v", err) } - cache.Close() - waitTestSignal(t, cache.cleanup.done) - cache.Close() + firstDone := startTestCleanup(t, cache) + + secondDone := make(chan struct{}) + go func() { + cache.StartCleanup() + close(secondDone) + }() + + waitTestSignal(t, secondDone) + + cache.StopCleanup() + waitTestSignal(t, firstDone) +} + +func TestCleanupLifecycleNilAndZeroValueSafe(_ *testing.T) { + var nilCache *Cache[string, int] + nilCache.StartCleanup() + nilCache.StopCleanup() + + var zero Cache[string, int] + zero.StartCleanup() + zero.StopCleanup() } func TestCleanupWorkerDrainsActiveSegmentAcrossBatches(t *testing.T) { @@ -180,15 +267,22 @@ func TestCleanupWorkerRunSchedulesContinuationForBacklog(t *testing.T) { } worker := newTestCleanupWorker(store, stats, time.Millisecond) - worker.start() - t.Cleanup(worker.close) + done := make(chan struct{}) + go func() { + worker.run() + close(done) + }() + t.Cleanup(func() { + worker.stopCh <- struct{}{} + waitTestSignal(t, done) + }) eventually(t, time.Second, func() bool { return stats.cleanupWorkerPendingCount.Load() > 0 && stats.cleanupWorkerRunCount.Load() > 1 }) } -func TestCacheCleanupWorkerUsesConfiguredLimits(t *testing.T) { +func TestCacheCleanupUsesConfiguredLimits(t *testing.T) { cache, err := New[string, int]( WithCleanupInterval(time.Hour), WithCleanupBatchSize(7), @@ -197,13 +291,15 @@ func TestCacheCleanupWorkerUsesConfiguredLimits(t *testing.T) { if err != nil { t.Fatalf("New() error = %v", err) } - t.Cleanup(cache.Close) - if cache.cleanup == nil { - t.Fatal("cleanup worker is nil") + if cache.cleanupInterval != time.Hour { + t.Fatalf("cleanup interval = %v, want 1h", cache.cleanupInterval) } - if cache.cleanup.policy.batchSize != 7 || cache.cleanup.policy.entryBudget != 11 { - t.Fatalf("cleanup policy = %+v, want batch=7 budget=11", cache.cleanup.policy) + if cache.cleanupPolicy.batchSize != 7 || cache.cleanupPolicy.entryBudget != 11 { + t.Fatalf( + "cleanup policy = %+v, want batch=7 budget=11", + cache.cleanupPolicy, + ) } } @@ -241,6 +337,30 @@ func TestCleanupWorkerHonorsConfiguredEntryBudget(t *testing.T) { } } +func startTestCleanup[K comparable, V any]( + t *testing.T, + cache *Cache[K, V], +) <-chan struct{} { + t.Helper() + + done := make(chan struct{}) + go func() { + cache.StartCleanup() + close(done) + }() + + t.Cleanup(cache.StopCleanup) + + eventually(t, testTimeout, func() bool { + cache.cleanupMu.Lock() + defer cache.cleanupMu.Unlock() + + return cache.cleanup != nil + }) + + return done +} + func eventually(t *testing.T, timeout time.Duration, condition func() bool) { t.Helper() diff --git a/doc.go b/doc.go index 186e793..c84cadd 100644 --- a/doc.go +++ b/doc.go @@ -5,9 +5,9 @@ // loading with configurable default or per-call loaders, duplicate load // suppression, and explicit removal. // -// Background expiration cleanup is optional and can be enabled with -// WithCleanupInterval. Cache.Close stops the cleanup worker and waits for it to -// exit. Cache operations remain available after Close. +// Background expiration cleanup is optional. StartCleanup runs the cleanup loop +// and blocks until StopCleanup is called. WithCleanupInterval configures the +// regular cleanup interval. // // Cache mutations act as publication barriers for concurrent loads, preventing // superseded loader results from overwriting newer cache state. diff --git a/load_test.go b/load_test.go index e100038..1906b6a 100644 --- a/load_test.go +++ b/load_test.go @@ -34,7 +34,6 @@ func TestGetOrLoadUsesDefaultLoader(t *testing.T) { if err != nil { t.Fatalf("NewWithDefaultLoader() error = %v", err) } - t.Cleanup(cache.Close) for range 2 { value, found, err := cache.GetOrLoad(context.Background(), "key") @@ -80,7 +79,6 @@ func TestGetOrLoadWithOverridesDefaultLoader(t *testing.T) { if err != nil { t.Fatalf("NewWithDefaultLoader() error = %v", err) } - t.Cleanup(cache.Close) value, found, err := cache.GetOrLoadFunc( context.Background(), @@ -114,7 +112,6 @@ func TestGetOrLoadEntryUsesDefaultLoader(t *testing.T) { if err != nil { t.Fatalf("NewWithDefaultLoader() error = %v", err) } - t.Cleanup(cache.Close) entry, found, err := cache.GetOrLoadEntry(context.Background(), "key") if err != nil || !found || entry.Value() != 42 || entry.ExpiresAt().IsZero() { @@ -142,7 +139,6 @@ func TestDefaultAndExplicitLoadersShareOneWave(t *testing.T) { if err != nil { t.Fatalf("NewWithDefaultLoader() error = %v", err) } - t.Cleanup(cache.Close) ownerDone := make(chan error, 1) go func() { @@ -605,7 +601,6 @@ func TestGetOrLoadUsesGenericKeyIdentity(t *testing.T) { if err != nil { t.Fatalf("New() error = %v", err) } - t.Cleanup(cache.Close) key := testCompositeKey{TenantID: 7, UserID: 42} equal := testCompositeKey{TenantID: 7, UserID: 42} @@ -907,6 +902,5 @@ func newPublicationTestCache(t *testing.T) *Cache[string, string] { t.Fatalf("New() error = %v", err) } - t.Cleanup(cache.Close) return cache } diff --git a/metrics_test.go b/metrics_test.go index 295d13f..26b5e08 100644 --- a/metrics_test.go +++ b/metrics_test.go @@ -60,8 +60,11 @@ func TestMetricsRegistersSource(t *testing.T) { if source.Name() != "users" { t.Fatalf("source name = %q, want users", source.Name()) } - if _, ok := source.(interface{ Close() }); ok { - t.Fatal("metrics source unexpectedly exposes Cache.Close") + if _, ok := source.(interface{ StartCleanup() }); ok { + t.Fatal("metrics source unexpectedly exposes Cache.StartCleanup") + } + if _, ok := source.(interface{ StopCleanup() }); ok { + t.Fatal("metrics source unexpectedly exposes Cache.StopCleanup") } cache.Set("a", 1, NoExpiration) @@ -103,8 +106,17 @@ func TestMetricsRegistersMultipleCaches(t *testing.T) { byName := make(map[string]MetricsSource, len(sources)) for _, source := range sources { - if _, ok := source.(interface{ Close() }); ok { - t.Fatalf("metrics source %q unexpectedly exposes Cache.Close", source.Name()) + if _, ok := source.(interface{ StartCleanup() }); ok { + t.Fatalf( + "metrics source %q unexpectedly exposes Cache.StartCleanup", + source.Name(), + ) + } + if _, ok := source.(interface{ StopCleanup() }); ok { + t.Fatalf( + "metrics source %q unexpectedly exposes Cache.StopCleanup", + source.Name(), + ) } if _, exists := byName[source.Name()]; exists { diff --git a/options.go b/options.go index 737b9b7..46b9f24 100644 --- a/options.go +++ b/options.go @@ -58,6 +58,7 @@ func defaultSettings() *settings { maxEntries: defaultMaxEntries, segmentCount: defaultStorageSegmentCount, ttl: defaultTTL, + cleanupInterval: defaultCleanupInterval, cleanupBatchSize: defaultCleanupBatchSize, cleanupEntryBudget: defaultCleanupEntryBudget, } @@ -161,19 +162,14 @@ func WithSlidingExpiration() Option { } } -// WithCleanupInterval enables periodic background physical removal of expired -// entries. +// WithCleanupInterval configures the interval between regular cleanup wakeups. // -// The interval controls regular background cleanup wakeups. While expired -// backlog remains, the cleaner may schedule bounded continuation work sooner. -// The interval does not affect logical TTL precision or the internal expiration -// bucket resolution. +// The default is one minute. While expired backlog remains, the cleaner may +// schedule bounded continuation work sooner. The interval does not affect +// logical TTL precision or the internal expiration bucket resolution. // -// Background cleanup is disabled by default. This is the only cleanup option -// that starts a background worker; cleanup batch size and entry budget only -// configure cleanup behavior. Manual cleanup through Cache.DeleteExpired is -// always available without this option. When background cleanup is enabled, -// Close must be called to stop the cleaner goroutine. +// StartCleanup must be started explicitly. Manual cleanup through +// Cache.DeleteExpired is always available. func WithCleanupInterval(interval time.Duration) Option { return func(settings *settings) error { if interval <= 0 { diff --git a/options_test.go b/options_test.go index 27a7990..4392b95 100644 --- a/options_test.go +++ b/options_test.go @@ -21,8 +21,15 @@ func TestDefaultSettings(t *testing.T) { if settings.ttl != defaultTTL { t.Fatalf("ttl = %v, want %v", settings.ttl, defaultTTL) } - if settings.jitter != 0 || settings.cleanupInterval != 0 { - t.Fatalf("optional durations must be disabled by default: %+v", settings) + if settings.jitter != 0 { + t.Fatalf("jitter = %v, want 0", settings.jitter) + } + if settings.cleanupInterval != defaultCleanupInterval { + t.Fatalf( + "cleanupInterval = %v, want %v", + settings.cleanupInterval, + defaultCleanupInterval, + ) } if settings.cleanupBatchSize != defaultCleanupBatchSize { t.Fatalf("cleanupBatchSize = %d, want %d", settings.cleanupBatchSize, defaultCleanupBatchSize) From 6a12f41aeb816fa2ebd0cf56a0b31990bc2a02c4 Mon Sep 17 00:00:00 2001 From: mkbeh Date: Sat, 19 Sep 2026 09:43:13 +0300 Subject: [PATCH 3/8] docs: refine README --- README.md | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 2825c7b..72cf9d0 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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`: ```go -cache, err := pacecache.New[string, string]("cache") +cache, err := pacecache.New[string, string]() if err != nil { panic(err) } -defer cache.Close() ``` @@ -64,11 +63,9 @@ deadlines and reduce synchronized expiration bursts. Individual entries can use ```go cache, _ := pacecache.New[string, string]( - "cache", pacecache.WithTTL(5*time.Minute), pacecache.WithJitter(30*time.Second), ) -defer cache.Close() ``` @@ -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") @@ -131,14 +129,12 @@ If the same loader is reused across calls, configure it once with `NewWithDefaul ```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") @@ -155,22 +151,23 @@ 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: ```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() +defer cache.StopCleanup() ``` -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 From 633930efb3f3b51b455a0cdd6c34708790015019 Mon Sep 17 00:00:00 2001 From: mkbeh Date: Sat, 19 Sep 2026 09:51:41 +0300 Subject: [PATCH 4/8] docs: prepare v1.3.0 changelog --- CHANGELOG.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29006a2..620802d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. From 698a18e1d21291a743f4100168d844e2e0633dae Mon Sep 17 00:00:00 2001 From: mkbeh Date: Sat, 19 Sep 2026 10:12:57 +0300 Subject: [PATCH 5/8] docs: align examples with API changes --- examples/basic/main.go | 2 -- examples/otel/README.md | 4 ++-- examples/otel/main.go | 8 ++++++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/examples/basic/main.go b/examples/basic/main.go index e39bae7..3e6e17d 100644 --- a/examples/basic/main.go +++ b/examples/basic/main.go @@ -36,7 +36,6 @@ func run(ctx context.Context) error { } users, err := pacecache.NewWithDefaultLoader[int64, user]( - "users", repository.find, pacecache.WithMaxEntries(128), pacecache.WithTTL(30*time.Second), @@ -45,7 +44,6 @@ func run(ctx context.Context) error { if err != nil { return fmt.Errorf("create users cache: %w", err) } - defer users.Close() // The first lookup loads the user from the underlying repository. first, found, err := users.GetOrLoad(ctx, 42) diff --git a/examples/otel/README.md b/examples/otel/README.md index 0215412..9e09174 100644 --- a/examples/otel/README.md +++ b/examples/otel/README.md @@ -12,7 +12,7 @@ OpenTelemetry SDK exporter without changing the cache integration. * Observing hits, misses, loader outcomes, and deletions * Handling not-found loader results without caching them * Flushing telemetry before a short-lived process exits -* Managing cache and OpenTelemetry lifecycles correctly +* Unregistering cache metrics before shutting down the OpenTelemetry SDK ## Run @@ -44,4 +44,4 @@ cache misses and two `not_found` loader outcomes because results returned with ` cache. Each subsequent lookup therefore invokes the repository again. Because this example is short-lived, it calls `ForceFlush` before exiting to flush pending telemetry. Long-running -applications normally rely on their configured OpenTelemetry metric reader to collect and export metrics continuously. +applications normally rely on their configured OpenTelemetry metric reader to collect and export metrics continuously. \ No newline at end of file diff --git a/examples/otel/main.go b/examples/otel/main.go index 34b1e19..7135892 100644 --- a/examples/otel/main.go +++ b/examples/otel/main.go @@ -51,6 +51,11 @@ func run(ctx context.Context) error { metrics := paceotel.New( paceotel.WithMeterProvider(meterProvider), ) + defer func() { + if err := metrics.Unregister(); err != nil { + log.Printf("unregister cache metrics: %v", err) + } + }() repository := &userRepository{ users: map[int64]user{ @@ -62,8 +67,8 @@ func run(ctx context.Context) error { } users, err := pacecache.NewWithDefaultLoader[int64, user]( - "users", repository.find, + pacecache.WithName("users"), pacecache.WithMaxEntries(128), pacecache.WithTTL(time.Minute), pacecache.WithMetrics(metrics), @@ -71,7 +76,6 @@ func run(ctx context.Context) error { if err != nil { return fmt.Errorf("create users cache: %w", err) } - defer users.Close() // 3. Generate representative cache activity. lookups := []struct { From 10756791ec41bf2f67eb9024ad5f653b7a1a8c33 Mon Sep 17 00:00:00 2001 From: mkbeh Date: Sat, 19 Sep 2026 10:22:44 +0300 Subject: [PATCH 6/8] docs: refine API documentation --- cache.go | 9 +++++---- delete.go | 2 +- doc.go | 12 ++++++------ lookup.go | 2 +- options.go | 8 ++++---- 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/cache.go b/cache.go index 32982a5..3a0428f 100644 --- a/cache.go +++ b/cache.go @@ -44,7 +44,8 @@ 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 and background cleanup are disabled by default. +// configured. Metrics are disabled by default, and background cleanup is not +// started automatically. func New[K comparable, V any]( options ...Option, ) (*Cache[K, V], error) { @@ -108,9 +109,9 @@ func newCache[K comparable, V any]( return cache, nil } -// StartCleanup runs periodic background expiration cleanup until StopCleanup is -// called. StartCleanup blocks for the lifetime of the cleanup loop; callers that -// want background cleanup should start it in a goroutine. If cleanup is already +// StartCleanup runs periodic expiration cleanup until StopCleanup is called. +// StartCleanup blocks for the lifetime of the cleanup loop; callers that want +// background cleanup should start it in a goroutine. If cleanup is already // running, StartCleanup returns immediately. func (cache *Cache[K, V]) StartCleanup() { if !cache.initialized() { diff --git a/delete.go b/delete.go index 9013a00..c43185b 100644 --- a/delete.go +++ b/delete.go @@ -196,7 +196,7 @@ func (cache *Cache[K, V]) Clear() { // index and returns the number of entries removed. // // DeleteExpired is always available; background cleanup does not need to be -// enabled. Logical expiration is independent of physical cleanup: an expired +// running. Logical expiration is independent of physical cleanup: an expired // entry is never returned even if it has not yet been reclaimed. Nearby // expiration deadlines are grouped internally. Bucket eligibility may trail // the exact TTL deadline by up to the internal bucket resolution; actual diff --git a/doc.go b/doc.go index c84cadd..03c9467 100644 --- a/doc.go +++ b/doc.go @@ -5,17 +5,17 @@ // loading with configurable default or per-call loaders, duplicate load // suppression, and explicit removal. // -// Background expiration cleanup is optional. StartCleanup runs the cleanup loop -// and blocks until StopCleanup is called. WithCleanupInterval configures the -// regular cleanup interval. +// Background expiration cleanup is optional and is not started automatically. +// StartCleanup runs the cleanup loop and blocks until StopCleanup is called. +// Callers that want background cleanup should start it in a goroutine. +// WithCleanupInterval configures the regular cleanup interval. // // Cache mutations act as publication barriers for concurrent loads, preventing // superseded loader results from overwriting newer cache state. // // Cache statistics are collected locally and exposed through Cache.Stats. -// Optional metrics integrations register during New and observe those snapshots -// without adding telemetry calls to the cache request path. Metrics integrations -// own the lifecycle of their registrations. +// Optional metrics integrations register when a cache is created and observe +// those snapshots without adding telemetry calls to the cache request path. // // The cache is local to one application process. It does not provide // distributed cache coherence between application instances. diff --git a/lookup.go b/lookup.go index 4426b5c..7642af0 100644 --- a/lookup.go +++ b/lookup.go @@ -45,7 +45,7 @@ func (cache *Cache[K, V]) RefreshTTL(key K) bool { // The returned bool reports whether a live entry exists. A live hit updates LRU // recency and contributes to lookup statistics. Expired entries are always // treated as misses and are removed when observed, by DeleteExpired, or by -// background cleanup when it is enabled. When sliding expiration is enabled, +// background cleanup when it is running. When sliding expiration is enabled, // a live hit refreshes the entry using the TTL with which it was stored. func (cache *Cache[K, V]) Get(key K) (V, bool) { var zero V diff --git a/options.go b/options.go index 46b9f24..9c26a1e 100644 --- a/options.go +++ b/options.go @@ -168,8 +168,8 @@ func WithSlidingExpiration() Option { // schedule bounded continuation work sooner. The interval does not affect // logical TTL precision or the internal expiration bucket resolution. // -// StartCleanup must be started explicitly. Manual cleanup through -// Cache.DeleteExpired is always available. +// Background cleanup must be started explicitly with StartCleanup. Manual +// cleanup through Cache.DeleteExpired is always available. func WithCleanupInterval(interval time.Duration) Option { return func(settings *settings) error { if interval <= 0 { @@ -224,8 +224,8 @@ func WithCleanupEntryBudget(entries int) Option { // WithMetrics configures optional cache metrics. // -// The Metrics implementation may be reused by multiple caches and owns the -// lifecycle of its registrations. +// The Metrics implementation may be reused by multiple caches. The cache +// does not manage the lifecycle of metrics registrations. func WithMetrics(metrics Metrics) Option { return func(settings *settings) error { settings.metrics = metrics From f766c14bf751ad55c56c1a6912f19dc0cad93468 Mon Sep 17 00:00:00 2001 From: mkbeh Date: Sat, 19 Sep 2026 12:27:18 +0300 Subject: [PATCH 7/8] bench: refresh performance benchmarks --- .../performance/hitratio/assets/hit-ratio.png | Bin 36207 -> 49113 bytes .../performance/memory/assets/memory.png | Bin 32186 -> 42123 bytes .../throughput/assets/throughput.png | Bin 26754 -> 36881 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/benchmarks/performance/hitratio/assets/hit-ratio.png b/benchmarks/performance/hitratio/assets/hit-ratio.png index 2d7e61a3383831d6be8cf1b22f9948b13d1f409f..c66100097934baa6276830555b46c7dfbc3d7ceb 100644 GIT binary patch literal 49113 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 literal 36207 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 literal 32186 zcmeFZbySyav@ME_qJmNiDo6;@AP6F5fYOMRq=IxQN=hh-0U`>50uoA>NH-{gL5Q?8 zB8YT%-TC6T*(HIp=yFsVJS>wS#U42?@zA+4E;r zNk}%oBO%!sMY<9H;#qs;1qq4U9oe&|)SO-ocR0FIHLk8rv|Oz}f1XupBg=D&jhmk` z6)_i)dR{)ocv~>WX^ZrS-JVRIp0wj~4}2OPo{p~zm3tsXde8rBwo4NUOsX8`*P$xOIL{ypKF}*Lz^L?}dT4O~4Vw^vm`dH~K25uNSto zwD=9y=iM+RC-*b147!w^x%Z-q%F}1hE>vv@dXAr#4AZ5hrRL;I8E#9nMJ_WIb#q%WKQQLt%{hB(q)1eDW@V9)+*Zx;SX=$^6=maHyXW-V@CH5iGJf3cPP&;e@d z7}bQq#-!%l>%ZuPEXvBu4~pLW^JUYv-3PR?t%AeC!m>D5ZhZav)t|-L#ifG#!-o$k zIytMWOJgb8v?N+Bl*=ARNt_PM3^o>A|Me+E$kL=bM18))z++_w&vfU`ooCOUHO22% zPQ;@g5_gG?ja^(=ux`yvux`tqB&OQHeW7o-CF2FRjz*qcAGzf6_r$FT^25iE3o%8i zs;Vj~D!)GYRDQw|1FFdljKWNb6p-?8K%l|a^58+9%G$F2M-=BFA1F+X>-JN z6V;QlEt}uE%nW+X*{!XvBx$C0=G!M>N@j+d$#?8X&&mqy;$~-`AIa%DaPXi`rr90_ zhLT%zHf(Hhm=#tQ7PYs!C;0fjTUp9PO*AB2^k)&DX}I|9+qbph-rxvdjH1wyBS)B- zqvQfPFqIQiQ=IMGhY$a(3O<&WmNq*(izQ=IAFJrKgL;Rfys~nM(?rimTh7YzvZ&)| zvPM$Wn>Ty+?j6LHvn!)0Y0xqpd42+`v$(kU1&`k3SroW}07M!F-~( zeK{)3%G%faH2}lL5h!;cs>Pg`cNZ6zyIXgOiHl=qZ{bR>UcJJ#V--VboNK)=K70DK zdthKc1;zJ#`yr;(JNRuS#xKvVyt7k6L?l<*hbDF`T;%#`|7gUF*0#2_$x7bOehjn* zpT7pMvC1Z1h?2Q^Gqa%J7RLG~_uAr6DxGiam5=vh4OmH@Q_7IA$I)9KXsoH?WD_$)9`$fRmt#u}HfWz#U0wOz#0 z+?c$7@2_7shMKsAg@w~5?1!3U{nZK_tnYrf?4Z&Ukry5wj=d=z9z;gV^YEatv9b7I zgOF{fu75OM`YU&V!$_iDLAH4qN1(;~+Z*=p-*3l|qOmlXlp5lDXnJO*qu8yWWmYpq z8&h@JCI4I4$+=Q8-t+#`9gbaJI|S*f3&x5qY4os2U9oM>sq?tbbvXL7%wSt#>+$gL z#CCPp#mGxDOguJdOjNfQ)q58ArM$fFLL55>hn&^5mBksq#8XL)Hm#XQ#l`h(FdP?S z75K%k$@+6>zAe1|OD3ZRJF$|hYJO?yftT0UgJpTz=~ut@l=~Nx#9U<{@G0*5@H1969@TZrwwr=%|^X8iQC8 z>i$kSLc*VAWv*n+ZD}M&F)A!Ibu9RpslqMnRtIF3@v$$6uT|bHS(Yzax-3N{-UmDmIU#M66o{ zC!Tjd+{0WY`tT?}7G<_wpInC2K1xdFQS-+KPE5{E{6ciq$+p@~N=lKV*lUB;`GO_k zoZq294)w+1j2fjd(JV}@c9w;#|1{PncAH1p!#&%`4=peBMGDS%YW-j|m?c2Zq((BV(PM$3AlU$wuk!ECC6W)<-ERV&)B0JbsQnH+YwfgW+ zI(7iETbP8~VoFY@YJ%F6Cr|KXFABWw-}e?fapQMY0Gl#$#m(;bchnQr_DtMDShB0> z55J}4AN~IQdjz_F}hwu)zg%kHhuI*9D z(oXU7^J9_4V+btGx7n6(%hvs=jbg}JS(xhgvCX)~$omP)ku!Y-*^5WN(CK$o@LcDu z8icEF-$s`grtEU=-P`46@>Rcz-sex&zmB$kq~qG%0VuTS=V zJ$TY#`cLhdW$yQaUQhP%pFTTU9V&cIMh0PmiG}5?Lf$3U!b^otX(EkYn{Mz+tj-%v zBm{D5>gQhj`2wl%Pn2JSMOI2mig_L7lo;kNMKwlFK8TAL739Y8s$(?|eY!IFt{=^| z?Q)+R?a)ls7r*)E`|S;zSY#6sSDTN zW6})uQu}CVj$1Uar}Z%$;@h_qci|T3_1TbyQ0THeMM9!XvGMS2JNeC}2;skbd)1P) z(oTErn3UY*g(&G`er$CrV(sOY)7O954F5=rUHR41gVfPrf!&)U?K2gpHo2ri;qrfk8n>94QAXhu_93 z#ZfZP@#yA`$Jb8^Z+xxQdAU9JdaRouonq*5Ys8CJVPV`hIo|s+Jl5RnZ45^q@pF=(IyniN8TWP4vNLF)i zka>M9wM1LbP>?+Jo$sGM{l?7$mH^CrYO^Pc$^gC+)HsBNbIQuj)~7srZsuTDrSOvP zlz;T@gQ8-q{B%3-17bbY@$eERB66OzZm$VjUSy=y&aSXyrccZk029i_)#@!H@o$gS zR|n!E&id%5+WvU=x+RaASxj;4MCjJjQML2sv<8T!h^Su}7M7=K${BBD{32lHz$i)N zt1B(rN%{bktTvV6z^S3xOY`%E*w~_B zO5f-WGedK7=6raI*&Qtb(B@CICsSg$%U2%!_@PxhiHDQ(r`o&MaF(TY7qW?LZ#W??e9^Rj_^@`0hS-ork;%4Q z#j(ut$fD(0vYW? z7&!M)%lf-7Pn@f*dn4^rSXg+{arCABCSaSM_`Y3-PC9UjiM4JC8JD7@q@>VLolhUjBzbH*@>yNHt7s@FE{pDZEH-lQ{B4two+lA3 zvOOm<<09N!t;U!t`}+Ejb%L`)`vS07FnPz>*v=InJ}T>fzOd)Ayb3nL$nbD!X=&L- z=h!_B&$Ux#XJ)dpvJ@2+#XVM+YopG2tS*@DxqXZXD-(5%jg8X-a@R#BZ}OHR>XqjS zE$y&qN;bE&q^G5wi{t#Vu&@xEBE)2~U)G;RgDq7(Ndq$^L|2XXH^{5`_3DhT4D(iD z8iU*Wv$RKk{W3B&eGwGo@UzqlTW|#novAQ5Jfg5rtX|a%C`UO=w9zVIMaGvdc;xi* z^;7!$4HN>^5PGH`EG#aHo;YD_V&c8wE34Dv?CfmZD=R05Hef3P^f0n7qN0$v%q0Za zYVERnJd+HHEV6c}4rgYl=lVSs`d%=fdvZX`NyT+$a%*{vYKgmBy}=oN7W@wNEC6bgVZwyLS0SK_dc<#rf=oK`5_;5-r zz(l6%@U!6Hh4}qj`cCqB4pf-*e0o0rVfa^UK-9y}ROQh}Oz7vPr`3`gMZo`wpsA1?n$H}*j?+NZX_-lG$+#xwpBl@fsjs5v#Fqoc(V{k|yK*w}dW zQ*l=fmmVwlRHl&Flh8cz>oaYh*PlOsm`Z#~+#aLK^#Rb$wz|ZB;i869v*BA>{`czb zoW%gW*xx{@WIJ{o$~t(x?vOpB$5&LM*;Xy-2nr~^45(D(Jg#oIoqiW<|LEz{r#oGS zQZSj^TJNN|B?_C*{u>7ZcVNp>vMbIlex4FT;eRYr~)M<5;_Ic1ZOvDaAR zPSmz25ZRPoq3+wcb0?Sr18wbAGTuU!<*NC~LsOHJ3Z~tHtTsj=qiNXxC>F3xqUAM-Hh%Eff0vknvwx_bDhIcZ)e3xMTiyobQ zqBrAP!akO6n`D>faXdWBbeu=F4#_%EEx~T0ryS^7(7JURp$h>Dpq6r~TFl$&zGC}J z7X_3GxGXT#%*4c{cb8A$*X0W*T&Dl%*cKVLrd)fw)K+$7H=SVomo29c*?ZqBx&Bx* z>@A);PAM!=H*XTih5oqJpV3hp1eC>T^NGvR!KGc1W_$dYTM+?suSX%J0(YVi-?e=^ zO3Ngzv^Qa49hec1`JdYsF0wk+p?=oJ<>KSxu^ny>)b#;C3){4tHNVySIip7s*^Vk( z#T%=?5al)Y5-PIdeJ8bhoi6aRM$Mm6kiST0Wo?bJn1CwCiWxZq0s>{#6d9)iy-u7s zv5P^%6=<3G;xW^2Mx7N;r_|Nd)Sd~eXKJV9_8Dedyz%j=e7Frc9Z-Z=ci+E%2TVF4 zAW--Db3afUroaO1Nlk>LZOD1Q11Ajg_gwEv_@Z;Q=e5u!{Nm%sj|MIStj@*3C5hvT z9N@37L9`(c9J`9$3_pEi8fniv#>c19Zr_z|T#45~V&Kuszv?n{QcSGyOHIfw?;zgC*s(qn z!DB^p`VJ1WSSMJ0S40i#FY~=T7@M0quj!Y@7brIakV~XDz+%1pn?Gk6Nm#r0pC*yJ z5F-}|P|d>18Ybp!*OqOKs@O1Owe;*>Py@mTKWO!o>F-$6xug(y#n#qfS&`$E0{0XO z>;B%};@+%?}mFiW31=X2{^EH{(<%Wxh3 ztSVGrlT%X&Sc4-Y*2l->uV4*Q`}inCNa%$Z*Hr6#kl_5pzcJ|YMKe*4l3uLe>wK4K!P`8_ETB5Zqzii*V~lAr8yIw~P@`r}+8B3cUD zIG&_onoTynu6}Ys!$~ahWwrFGL)q2`yXZ|qw%Ut^wHWL#i?IXrSooBBZFaZ?tX{DF z>cuD--}HR%Q?Lo=4H|K0w z{!}atkFE&j4Xb{2%CPM5vq`Hr0{pMbpB_G=Ks)UI9Qi8CrbCp2W3Xthv-0cL{X!NE zzjnT&RDSM;^@rWXcZ>7*agE2L^oSkHJ3CS3Vd)tkKl)HEX73r(a*p1ft0BIY!1Ogh z_vK|NZ(_hDVe+=#y@IKU74N$wW84$lgjH5`TJP%Bs}T}zWp&Htl@%3xEe}^phHQ z?-w|$V5HN#aKd%|nwHkffrT_ARe6nyV=X(7fPkn6fstn7&RGwFIi=yO3{`wjdGKII zk|wWn?elY&ie2-3eXCLLY~8uhm1JugXQ=9eLg3T`0L@DNrerqedOg}Z-Zt*LVa;-OGc zmeHBvyHou&IaygVlav0m-)IlXMY>0v{#A}E^;pE;VEnmV_9RF+scyHl#;-IX(cY0%Y~RF8%EdE884X9`uv&aHQ~ z)>06+u)HX^CD)dT*in8jt^7cwR$UaJtG>Sel`B^;G^x#F9Rz+J_a~kS6@%LcW0?ON4^X_Y(%DBIgU3#`2O{aQ)R9OcoexI26R;>n~2PNzl;qvPTvEh=6iKYjlE zIn?3XmoF4aUw!^C@xJ;qFyP$%enaiKq(<-I!^+PsjEpiafADN?*X7^%{o_X?KpmMV zeqP?1;b+Gg7$OKxKG-yzzgo$D#9+OU%sB68}s7yQ(yR%fBbkRMI%Wq5%h#g zRPDn(egK4%hSRTKzc#5qn9w*#{Cj8XuWh9WHN|dA-oD@yE)<8IG_Ig3(q9w#3^Kwb z`?fc>#xxj}{{H?@htc{tKi9`SGDG0G5ulx(qO?$74tgY&d-=o0>v}+6kDLa&yWfH5 zbG8Jspo~(swz5J(En`{&w^?#?!2i6ZgoH$oXRSpRVu3(`xPF2vYrO$$hSbZaPcMs- zr;w(3ez;6wV0rm+_pet>Bu;M<4$X{jA`u(-79r{J=lkp5)uE09b<#GEpFHXO%V8;K zvK83%Rp1po@7}#NGiaRe)!WLTGWA5YKAI3`7+<&~g;xRzgk#~blP;j~_MF-0kJS|`eoyfKBs3U4(?#sjoqa?*T zPf|~6!V}QyJAJqrB3Gne0$~Oz!^Fg-DN)@Lmoj`^7$47`HfV$jv)6bCjHVl+FY=C-^JyzrOpA{pJlj6IJWf`{fxpm$I@lFeuIO z7nu3@u6=oWm?3H zxPkoCIh_u^wHc@YY3OP`Ur~D}rUPp?Bzvr@WDPL}ETf}?!){X2KM;3*@iy}uxW$N= zbnI9Lk}_uJpupAhKNqaZi@w*#DaWl#RlAV!)b`QR<{Sag@A5I_sHW+6-p4AgFZo6yl~)!mKr+6$-XaT@%?_Kl=knj(L_LaRw0-+_LUT(qIwvJ{+lhplq(c1u zJ>HjIyyS8^#&;G-x|sKe90;*x*`6;FYG(R z!`>@PbCpwzk(q(jhogMyg!Xb)Y~Hek;2nUYo8FR=aNUt2B~gwBSdEE^$uh4$A|mpm zHEa1Go$qfDVzsrk9D#{89r=(*4HZe{Tk~$1Vg@UNxRDO6zA}0P#)7xbz?d3phY1*B z&+zl}S08eEH6ykm(iN2yA%UNs*v3a_;VSGK4-`=SUSS3HD1KJ4Ldt;SNYUtNbK}Gx7K5|lwqCgB#N$I zlfq|fDL6f~1XW`YRMN#usU9npA3y%W>gh3db#=w70%&)DngY$KTj*5lA49y8RGw*` z)I8(|UY+b@fNSYuB36L0lJ}2RO*mT_e<2D9wZ6VSz~3MHurbv@vPj=H&4PHZ$o33f zM&$SzaD7OdAY58>u(S>zK8y;%3uIOYA`^p{(>ThQoQ#Z%32G=Lm+=?GS<8QSiC1FI z>DnnglPLrBaUf9AfL`n>L7%B}s6#$2mp3)-C(4Z?mqyj{^=Dqtym>k!8Z!7jFE2gp z(@e}&ZAXXRAqfsy|F}5Q)2BU!b`Udhd-7|=9X@7eBUlZ(+~&dn)*KwN+uy1tRLKO< z95P!Z51gJQhOc|OQJ<8E(Wl24uC^aPKrZn~N}`0s1IIU7FfcHrZT9l5X1J?;QB@V= zp(shj-O7vpG(mxZSxD=6KC#3zaxNy14(;%ctsiR4&Te|UaefmmO9d6r`R7Mp=-T!vw3$fQuX+QSb2-W;cRn>DA*{E8xtk&u2o3j48 zdU~44T8HPpe)|S#w+A@p?%lhrvUCXgAV+ebtZm!&z_HVrjgOCj??DMP#DF}1fBMci z1_N8o-vCnadHL&X27!~(S8&;Rru`cH1bh9(~3aoz_gCN;`POM{(LM(yBx{rYvJTa-M+IML&^ zk@SYUyD1JFNY)5_@T{SsAv&5x*1t|5{bIW=hgy7hf4^z#VNkLlxRJ^srrJfV8y%37 zl6CX!w6wIGoaP}p+u7NH1w6YAMivMi&s%FHTXN43L8g0*jGWvS6$U6LAc?+DP@gt$ z-t0R6^8sNF0bAwnUd$p3Bu&0^=PQ1rPxu!Ul{9eCWG6uPBoZRDyVRW?*YO8H$MK=@ zUw)EJP#-~C#QK6-hzCE4+WGHZ0z7Ax6-91@0JT+uf{LnlaBvaHKj^o)lj`62wyY>@ z+lehRj(n>?pQ=|d+j(p3ptuV`oq?r;D&LuD9>5}-S~S}VaSz&$11fn~X$GVGBm~;D zU0fC+q%tuvDXrNZXJRr0(cv|>kBW+L5Ebe3<|OMEiL$ewUtXTBS0+cwU$(NfWtfn% zuoy%c(CIp*xDbB$>fg}n5db$2Z^?39vD+A5*_a&nKwfjCxKJK&4myOorc z!OR0Y-5Sm7KN|s}+_o*d+R744C{}6tSQ<($keI09{g$60$lM%lzf`E3X?6jnM76{l z)dUQ4q-p|GFWCb(^Gc7c$M)4Dyu7@!{{8_0iiEfiNX6*Bo1DA^lZ+i_ZEYR)>J{+P z*#em;Sw%KrBlk}m-fpFQKh{QmZj+}t4WIUlw$z+2F0dlW2M!$A(QLxE@5zA^OtPwAp-^;v@?$VS`c@U$rhI&KJ@2qnYrfSTXv-NRP->2i zo~-}1_|(hOg!ueG&l z2!MnD|M{FoC*M^nBJRk_$;*RjY(UMSSRctE3*h7mTS2*L9N4!gF#9yUQFCqb>eyt0RiOw zGa(HC2x5?rn>SqUs|W-^+n(Yg_6m33UcP&ly-DSo!s=31`)(PoGdkC)Zk$f$-R>UVB0s5mrY-oYA%^ zdE$cChn=VP6CwD28EggpUjJHu#=*2Xf1#$gJf=u$}e(;V9xF|Jw-eKbd|0Yrn3l3^%Al=WOKOxfrlA%HiYFnp8uEN9s78dL?m+pG*e{LI8a0Z4ID73n;te{8;&4wnE z2h@mEMK5smAq;7N3eroUTLzm`yPzVY1O&S_1M3$JCp9s^&)?hK8E-L-=*Y6vMB%7F zPf1C6?(x0}kPmDuEbZ9rh>9pI`00H8S-kGu6SVCVgXIqOHc`>ts1c8AwBXj@Ucc0> zAhLURw~%I+-iFuSpQkbO2Dgm>st$}~|E=wWFLSZ5z$RTmj8^09-+D z`un-c0{auCxPG;$;qLWoJ`(UEQ#GGnwDUJjF1^eb6xu~Qw5=58U0&_XKVP0Pqm;5U zLL`F*+F8UY3bR*vWhKFs z0HN>PwJSyCb!nz~eF(D6TfLK&YGlL;kh}a^$_c0yV_V64*VB_lb_Y4RqVxG=U)Bx%CX3EWKNXh>0O#Ej@kb9S8ttXJ@ZA6i?z14wxiWU!V2@ z;RC{b@TMrdqIIczhz~z6U#y{^sTtj1sk#2;zNdXOvD?;vbM$|+?EA0X_5UB&tpEM+ z|I=g+NO90o`hlfAc8T7>YqqdNe%iF|tCq za?9y3C1JPbNv z88^VhgPYS%*}Y@O-Vyk95Q}{tKhD(3BKDZd=p`4`{m0-;M@K;{H$+nWFRa?q**b+% z#0S`Ok`IugA_Z@5?xPf!;qg9MFR2l>;LvOqHa6NRwJRWBCkmPdraf{jmbb_-$I%&+ z{#*4fXSBGFb~SNwddaX)>d&uV0k2+hl{5^^faOIhfvv39#Jr>kFAFB(mHsZ z5t7=@b@RbEkUCl`{0+P#YF?eV2#O!Z090ARX5Zg``0z8YKU^pM>Z3nXGtCCd;bM7c z#DJT*Mr>-Qr4lhyQeV?m7?a_yfS=@IUR1v$jWO-5^rH{6DNN6ZLkB%t# zQ2ixeSf_&`GnWk*vF`{40+}43%7ePyUV8H^6+<>Rw=^3#QZu`%Dep(B(0&K?4 z!ctaWKQ&b&Dd{|U6)qNV2Vi^&?cBC_;Rv)1xQ3yaK+8yXs9yi}-pC*pbI`y*kfQ*r zh(@@l*<$vC2M>T_vE;>3*uq5xWS5D>N??d%CXZ(}wjjX}Ci#{S5S^rN@4$@OljKkfAfj97Hf!H7$aHMFaN$1~gk$YwzHFYC^;6z4A zi3jMhjT<-iD!~bX$^s!rgJUmoZIwwp3;u;5!eEe03J3^T#wJ(tX9-g>_1&?Kq+)vf z4o9K(^prMc19gNtK%qDFUxqj;$Rpo*DpoAe44;Pb zzdVU-7NhwAs@`&=roqfWJ=@8Xx8S$;q2)y|&18|K<8V_-z5hXQ@0L%gLw2rF^KS6#WxP3`XN6k zs`fk+8WCK7z~G^~uda{gGNs4nQuO7(8bI6C2+E<%k=KC^-WdI@+qPL|wPLO3=H|l4 zqgds_AzR0pS~v+Ne+bWg0G#WiX5wK;DyGjdW;U5sfiV#_ue)0NMmoZE@{v*;`f-f0 zX7zM+ZMurBO-)fl>D4MOyTU}CM75pxW?$Xf#w(ZwZ~@p}wPsaczBs9AqED|b{xaAG zSe7p3UIUA4_C4|gbVC+dIRyp5wy*~e&S6iQ6O0IP_Yx@)({Bg(uL>SNo(+)UHy9Ea zXq+!7{ph^BJfSkcr%ZH6pz8#L*7RrxF>7!{Kx)s7gX&G6;R3z}lXlMf1V8_ULyxps ze1p^lxjvW)+9h6a_;+QEV3VG4x2&mPP=w$_iPR5R@?ne3aWZlRTU75a~yiBVmVd8VWSiS;1{>3#FnL|Y!s8zFOI#p zdt*8OzQ_MDBj1slC*SAW*FV?iap&E~P5vzE$y(RZeh}1N^x-l)JNr|EL^Y0Io8Z&4 zcW{uf>phQwXO;E#@$q^1kdMp>^Criyvs!;twSz-r{&U~28uLu;|2t_P_^IjBr&d<6 z4^~!ceZS|*E1FN?8n9d9E&*sB!7c{U0^gPeVF1h)!sPotau2a>*tOb_D6szP)ZoHzT^h6^Gx3)%D1n1-9`l$gLf*{AA_Pi++?>lnZ zT3g*Gq|Arcm&N!wMu5+17?$9Cb-`Rf^_5^@iAFu#^mfSxw)@iAKF3D`#xDmsKi-$oe1W{%U0UInXnAQv@XX2W->)yCG(8IgS#bpIZjAdYd z?b<#ur)x(QIo7xMJ8!0qc%U*!WQg`JKtm$j02YItn?ihQVuw(QklXH!FIjNnGubGiGAyjBUj0A=NdF;gxJiqG=hgk&;}1|Y-wpZ(n?%|B<7EwR4$a4 z6pglM*#J;@Fm*m4noWPcMT?pv3`I^}#OH3SPMl*rAZVrnreZ?R+R8iJ|L<6{EHYE@ zXlH(D4J1BB!c`&T9}UtJ*joJeHV}>woYgDT|2|F!c04vO=0@YKZUU&!u@w;h66j?F zx)0*`m$#xNB|Ip=dqi6*3I^C;;eYV1H4_}6qNbifCOdB1=?cvN`W_h#*E{_W#GP;Z zPkcUh4^6l(zk^LB7XL)$bh=m~nPR}8iU5bBp8>r};MUPWhCl!=(-~ZRirz8e+mV}> zyGp0gxdas!414XJP1_(s1VF713DL#_!*K~|p%9D+LY4;idikT`-(Zr8v0a|rq>;E@ zU|&AW#r^+IfBl{DSkC|X%a^&>0?6Gkh}znY!H@uYJQ^G9U#GZg(@%6265SK9*vR_B z<^?wmi#aA0xE$3|T%h;5(EGgb!TrbR6nnxTSqw%9ivcx9)4)x{e6gpW_E4u=m&ag-UzYY+~^EJ!68O6WF2^V(h4wslonWw)5%RY6Hf4~Xb+Q2oFgp`Aya zxIuEV0RzzxssUjzzYz-uL0f}kAMuPT0*7|oD_L7z0zJ+mD>{FBKnEghfX5*TSCPMJ zclb^6Ev48bL=RtQr+$p;BbtS6sQ1=%(O~A&yq;#*uaIv+Y|(=-!L+ms!W1Y##Off9 zo$L3=ucD28!E*HY@mVAWqT?mz!{uDpg-K%Hab1FOf!arNeMZ@-f8V(VYuT^Qe$8fr zCL^E@-|FV2Zz20T0BP(jcaV&L@C3nX~Tr3{(WW8Ssi}`u~C*RT$Chg;fmr;Lnnmo6Ew&@^BVn z2-rdzG`J_6CtgJA>n(N)Jhbr9qXbs;XIWTUf<;Lmt?|yrnc5@Z>{I@A>ZwSJ3Jdk| zoTz?>VKc&*!{*VfclcP zl`&XGwEyKfj*;KPav~DpyfaV@Z{ax6aE)2gz@%6{U*9L}+|pbMyt1nBo&ag0n8td! z1cczDxsFWO?T?$JmaED zal9i8VVLG)L-3x+-@3JgR(tL21YetyG#W-q6t)MO@zqB|(%pQM7D zfhb5&OQ^$=MH5}$CwQUJtb5$*$M?$0O1gDKNFrB$rcw#NNJbwXs1a;9cun9ne+!EW zR@H%n2Lafw{!2!waXnZH{N9conUq=#+7M0VlQT1f!U}%4u&5|S?J)6lk!n(gb z09Km&*l8e0nv(vBj@XKjk#P_M8mUTMX4W5r>n!?|;O>PjpGnqVE`~^Y*o!cbLaV)a z?%cV0VlhbwlM^{4vVGowQ4AFYKB1@sh8C`%t4;REw||*Uc3~?P*$}uICivg|{<}=r zj{gG6IBx9M7#@j+4WXk)yA#|2R1{p*rPtO5!|DKy+sFekaTpp2^LtPv#U z5)VdU;pTt83=x>o(mK$dmvJc${RM3;EohZ)OV9e7MxV7#d%-_qVuCivi%AR?HD4Dz zhC7^f)r{BHCN_QCo$j-PgPqQ$Aok7CH}TzDd!O8Y8lU^ZL_6eCg0V&HC3EATr=F2V zcKr5FFn`3vlVjHHNyg4W%EZU@@%Yp&SMfO`cS=wDt~+;c=+}6J+z1$)50e0_urK_OXN zScoEjPEOj{N9poJ@EggXnx;dprKQGH#^Do~-n@Ae;jxB|zLk$|AAT_$Hzk;5t@+x|70d z6<8sD!~TLOnd(z5=`_yB!(;Qob4$-vDXI6kIEp|xa?lt|lpASourA%de;>@0{e|_d zxIPZ@^74Il&bVKWRm-H+ar95ApQ7O6;-cZwdKwb4*yF?dEmXLss)~!78ypxq*`}d+awx-{|zxvgQ3_w#r^A_N8^sbdZ zJA=($9o};^xh9f@`2N#}lD-t^>LI!!5O%uIbpneFFfZ)jXqwe^4Ao3f0}e>iNEt+{ zEF&q3F!Zu28XCTVnfCPS@52cy>{Zp(LOO^@fV~FwA@y~27|5yTikxy0(mh_PF+C|V zD<3vIa`D=bx_VZ4xfP3n#Q&)d)GsPBw3sBF)lnll`~8i1JHY=lE4 zpul{`)UAWXF87m?l3i6*$qvF@XuCnkgEO1(r+6}=IpTRx&~Z5O!C+`;XgnZin4O!W z-Fz4{hmepxPBsXsL=$(9`AHL%>bK?=muIGzRK{uoM0IMvEO zD^-Pyw;_m>mKvh00TM;|kTkQNzNFF)c^GkFB<{x^ zhg6Be=C7w&J&qct@}i|50V6&PN%bkj3cr1IkTFzDJZGPg}4hz&6l+;rSN1B=Z2uo=qw~3z28s&S{+5t7*O1UU$C#gpUm?+Lc(OoB`qzpf^`m7Dy5b-;w*2#*D~|F zgJ1e4;h9j=(6IQ|U#^`D!>JQ6{FWf9=clfxsqx3lDSt2hUHosxc>0ga^87z8OO^_$ z-yM_9Kke4sT0{s8J zdMW-LfF?_C9j0~%_r$J;9>xp*5BL1X*}3;0*7(ssD%sh8oSkp}(!T%wb%eS9I9>mT zhHk%E!R<9(X69d*#?{qTLQFnQtp4@G{>+5_2{-jxnjk#_e=p8Ua;D=E(>1 z4q_!PqphQ3nJV+o#SQO`L!AM|-1Y9t-F$+2g4b>L_U(Jvz{6HoR~s1_Woa@KAO1r( zvaJ-+6Bcr=OX&o99{hw{Ku=E(Dnsp6Lvyn&6e1Uw{Mm~2C9M~=B9)8wNx0t;x6O}B zIE_oWS?+&^-X{>!;m-ENMI$pZU3dK?x|HB!f|GA>V8A>ZDobQ^^x2FUu!zvEL%8v_ zF7xA86fLn~?4WLkrw@y7JC7kQ}IC4A4EsNQ#4I zJQ-n9oP_(&0q(WRK51!b#2&*Mq{5RYPMGuPz(4=ug=T$7Qtwx`gy)Ict=q+Xo47uoh=>R$r@X74_M*q(0uZH$yr_n=GBQqr!GrgB^C>7(MIa>*p9No_ z+19xtgp73{A5 z^WaK;eEc88)i9CkhN%5EpTa>YqmV0kNIkv0%Hb>kr=8^7yzVf1FR5k_eukaYQ{l+e z?5suv^Wtd1nD(VhXsarYjxK9%o`Z+E+2PLm99$f{z$nPikEV13K^^^D3b&{UxOkiA zSHl(J_y1M9;=kjO)c-$Y@p?ETlqq-#Pag>YN7mNXrqkh$)@$HOYA|mD^`9#%<&*x# zsJFGNq*u(${-D%F0~~62z@pV}C&wO+fGTz#h54K<&Re_?E!&;L#N{~cUx{~>x z2XHug_Deu~qBRvs8_^1ohf62B>iv6yJBNgY^Tom$Nr7;HB7n}peYA3~vqCrT2c07d z3O7I>6qt_!;Qk5>4BX+>Q|d(qcjG=u_rf0%m*0~>q(-Mo3eCDFVEw)*XAzz?lz~bX z(e!Bcoe^%Nva)EfHHHQ7zoLm8?FLzzM~Nr>6XJ6H;{IBHR(ZJMUp;-wNO~G9H170U zmyg#ABKURNpDR2NV;}I5?WjLF_kQpv>a&XiQUj)8oWg--E)VD$@Q+Psj z5;|AN_{dRZT{yYwshZIHAK`M~KVeTzbZYL+ij8Mn196r?>fPtNOCpbX z@V1iQ14GEo$$1h6By_Uo6MfH+#XyEeY}~Y|JoH&7Jj-Zq3n9Y^h)YM4;FWh>bf{MF zUAsOqZz;{Xqtl{+;cti|0JfZOaq(<~jOik zuBy5h$5`NuHnS{M;`85yw0GZ=@M{zl{*z{^vo1iM-x9Zb9g6-R#YC$-F{;imwt9Q- zu%_J@>9F1v5N?a=^_G+rtl)_v!B3y^!Jq~1*y5zAi3#C8L)#oyF_}76$8Y!^y0+}oCfzYIGE@T2Y)pL$bR9%g+K{3q<~G&GSMjXSX)K0G&kJB zOzMeseERh@JG;G`ub{mcTL^+R(MW#!_4vfZBa|acVOv!~*CAz1QhK@oUU{-tHdrB6 zHKA-zAr86%ptzpyotrBXtdNbI5B6)U# z{<|guKfJTUokdG1++l=kf=3RB21uf5;?SmeV*>l$?hi~$n*f~gef+qAn<(Ye(ylA6 z)BvYLb(@KP@PMe|BZdhF$oTj*`{~fOP3PD!C%8onVj8^vJ3hE*KYFr_wK2+;lgl|5K}>% zTM4z?9r`{ZMAM}alEHg-vWb}V{ut~?0f1~tY^c_V>ejjjpez&Q$FXBmM9=5V0aYcX zZvk%T$iS&6Eg*MKQM7k-VB3!4P$w>kE}-mr95+f?8{PxYF2#Z7Dgu0JYjcpEmX%3c%GUhgaI%viV6_U=Fp?A zhE{JVA5=S}(Fg*}2~$=VbOpQy{1C(;7|>MUaRTy%UjbYMw|3?}k!zO~IU~@Vk2VSi zbSqrk7AS}ALY|3*2O!t6c3r{Apm$)<%5g)31ktN*9Xn?{;V} ziDPH!07(xWa-ZmtHZbUV`jo5(Hx zpj+KRrOQZ)%9)d$o#KTB7PpxhaSlnu*U^pfLN1LGQ8L>IbrErRVsf(1;jrq9u+XmW zO>%AOBy0bkx7hqvRx$5l@(4$`=96rhr7qi~5e|!O&41hJ{#OG``P~6vv2ZWW{jA=0 zG1*D+wNaj`q03CxnirUj&a7lBDrV_ziM;!Fzm@iUX=@(i%+xENswn?(wT4O9T64o6 zMy37h-1vV*-B+_APj?lN)S_Sq$?doN-yQJqV#@F$L%DSt{=tufIMK(9R{bx?u;Mw` z1c-0yG>>pxsH|+TfU&oU5iC=Ijf-fr9%{CQ6=F~(P)-PJ3s_>fP*}yq3BFz)1bRC> z7v{+Khy{ecAY4F3^8Gpy1y;$hPLBx+wqwsDKAk`R5a@UtgKK2CrI}eSHpxtHjpSOP z|W>8LjUgz|_srR0RE2Mq3iMn(uf zIzI(!fP<3m%TNShxic^}?h9Cfz(BOiph)rwK=45&!~mzeNN{Lq0cb0>7x^gSU^c>e zRF>wn8%XL(NwNFZd?2u3*B$2KN^Z>~XOLLNvAN{|V&zivn15|8t=#;4H1Ym~O#60r zH$VEBCc%RO+2Iu897va_>so&NP%Bu4*<>F*{fofBK2Sib3;ob1;v2&@k(}Q`1eaw; zoY4mKQ?l?o6p>U%M@O^G6x1U?7g7cd8_sru9=3;)l2E77O^S$Tk;*10Ag}`#%Wd1? zQ-X-8b#Ygh$C?{Xu0W#@!s-ij)C+eyMa|BVlQ15BOtH~Pf`DJ1jPSxYHT(pvjI)El z-m2!{sMOEv?c+J-^`CG$jKP|+#abB<^%_~pt^(gRT3H;9s_Lk zgaF?u3NUU&00t`Bv`IpeRW3n|ICBEf3CD{B1qBJ&w?UsrKxzzt*fH3QN)$6zXg{Qp zMWz%Y_!Wv7x^ZA-oS2>MEO4~V>LB`7G6!&!0kn1;meLCoSKm8Kf6l(cn^n5-WH^%3 z9_{?-UX{)|h-#fUTmlpU#DjSHV*&y=@D}E(3zU=rSVX&VRAPdf1y5jb@QEv>_c)mO z2p9|tm{a{1w6%Mali)Pe9lLiw@%9#V8o!KS4v7yZ6U^ZBOVKu?21~RW!Qc+iw?ECz$!ke@^W%u$%w;tM;4KUx`&6waJ(B%a>=Sl-6xcX zW3IBYgv=>O#wEKw4v`ENfRw|Dpx=Uzohxka34n{q;KGG8uy;r@3G|2|pdCsuNT(={ zD7*x=C32pi`CxKhWK4{)v-1LG8QHEp2-6IwFm(Ivo{_1kwl+3zM+W|Idcct8v~>VmWY?};jr~5z z_LvU1bI=_L9tKAYqK%I@m@{v|tI@_7`58oP`~WRQEd)-w<5oB;1a2m*jRCBh#@XVH zBrNFljw~izq*x6uH~VDK`y_nq*b>^;1?f@4=D$=3Itcx)?SFIJ*BLinw)Hf zTi};8n_jCXw4=oxT_@xRL>Ng4Hx>?M#`(mFR^P_?tH&0f?lv;|g)M594P|PFCt_GS z8p?qe(rQ~;PT&xf8?H5Itp->^|EFEv@?CO?@u?|F4@Z@Zw6q08x3Xq<8o*`r6e(az ziT&XdNhz@Ztkpi z@4j1AVG&kW!(XJ~!LNFeBM=cLHZ~R&Rmg~4o-UwLX(^MCkPuwPU?iX-z*yJQ&bD;M zB3vwJRp|jLwSy37K7WqGVuP`S;BktI+#GC*O&DTS_wU`?Gdfy~Q^YP_tdBb9cigG;R=B8zr*I;vebq@N#m(F_?|BMzgZSi6$RQQfc_@A++#$F)K=oM(SM(9Eh zWr(>7#XV&lQj8pfn1?JqkF&pUclAEqlOX(Y)-EFF3X0IdPbd_DM`0KPmTYe)PL`cX zMyP}D3WsK(sS-Lp&aRFAHoH&s4=DL#LP9H8M#OP?0LF(T-Nm5GwT9#9T@Z3Na0COu z8d7bX%kWnlqN^53Di^(4MEpaD#M-kjp~MTIhE5PNyR9E|>|fYM~d-7fDmYR@Yc!NLy>G)x)AA{uR@8256Iw<6k zgRm%n$3B@)g-jFAI~DNtDnw^YQOLoD54PB7*!sk&4v2Xv$;s1)ZY@?I`#PhS)8YmQ z(f2p*;mJT?ptCZ%Sl}J_P-O`&qFo8{ojM(1=767*nwFNRVBtJEgtI^3j7^$Oh>s`5 zjmXF-+0YT+YnQip^ctF@2IaSHIhBj6;PfiueA=&U5jgM|2TeB37WXVL9@+{RjJ<&= zI2nU{0hq@F(-khG93E6IC$wwKq zE7m%SL)2!evqO`}cd?*l5z;_Y0I4XxdC03qvKOzjE-|4#Gh*t?7MbCjfCY(Zlc?&J$C}( z5ly|XW9CrYM>bKAULa0u)aB7s1`G0|wRLr+9O4k&|5eW^l z3JDQS*`uV5$&wL+7E9AY8%>i(%gC5SQIVuYVrZd7!*oQJqEjhGqp`GTv-Bvb=XLYr z_Ye5`VJ50`KIikf@Aq`RJy}w0B?76l;WJez8#A4$&w|reXKnT9>kahQE+C3g&-UF5hTuH@{_##Z zxnBqy*<0_|-#V=$#=>A689J}vnIb0ad#<&Hl}3doX1e;a=JVtv=_Lx|(*2(9Q-c;0 z!fjY#>%SnoY+=t(tQDN;PpTJ#W&Az|5@1*HkX+?uvV|c^v0q(7 z_V54D-OU3-8S<{JZhgSYpKNU((AgD7&BFH7_r;x2SJq~(n84^aG&LBQDbh(IJ!6{W z{WGUe3lbA{)FDAGa|ZYUPA=Qv)Q`>d&$v#t`sbf!Y4+4gWZs$BOtkJA6STbR+ZV0z zakkr5brk&MX{e>tFX-NQrBh#n_2mP-<8{F72fhA;82}7Vu6QmEDCFMioLE^$D@_#3 z%F4pl3PCdwPj3wIt@y+I!NF4ysZyxM#>6NDD=8^yJ~5Y7RM-{R-o|aUP(RkP3O_VC zxkv}@{K+F7TB9pTj11zR+Z+M#AbdZnn-0JK_Gn3Z{Ev7{G&H2{Ip-8sLrURkezSdD zp}u%#-XX_J#s7mAR5lO0F09Focb=ppLE%H8Tj4WHeDkDTQ=7sD=7;E%Y-|oe%3?Mn zy6tVNK9Mc6lM^D9m_ugD>KwvHWG%s(z(BiweG5Mu8CLnYND?+bPN_E3h<3{l|612U zFR<`S+Q_86)2DrK^;4u(guOkTFyK18F|~?`rlwgOU>a?5Xhg(u9IbPf$+a8xmqz<8 zRH@CZ_tjb@tJ8VGFU8wiTaOZhj)>q33k&n4H8o;JV72}y;MQyD+#w+>IqtB`-35}G z#Mh}O$STP?JARSrZrw?fWbXV2bfwZgEFuEsl#`X4F}ea&y9pNu`Px`)vriA)u-G$V zS#)jYsYOJ+0R#GrudLjMEb6>a<)boN`glQIspT1Z?bE~M=WeTRS3dKfyf^pltz(Wi zc$oE;-TC#cv8LP6*kJ>o_1$|MB6smf?5IKNzx7S?91$2lOY68@YGl6OXas6Gz|LL@mP(Yo%a7#39Cq83c(Mz`aDNQCTr;-hHy!MrUWL zhymDS4*e|ua$Qz9RNyU=pgcQk4HNOxl2o_q8^igX-=P{y`lLz)V z5gvO^V0YFL*N{lM=%GV~d?ZX5jRu8BuiwIAw~94f82E}#g7}8hXIefXpXt1}w0~s< zt_^2c9oBKi!r(>}8o;vB8-VKjdTQ8>{mm%>F?*fm-CZE^(+GDXfs;l3bR~f5MIiSp zO}-ejix9}9hRYs_ww4y-(*dZ79Q;7gLqbB>$aCI&?LF#erR@31ws2ifZr=t^*VEEk z_;)Y3A$9>z!LjO}QOFO(ArdWl(&DdLR22U{ZLcTf+tegPE$Y+sN9b*EtDy1#J#PBv zpD7qzh&%;0`CSjmcAO6er8I2@dV08{o$U1Kmp6XgxI`X093!6!Z;aQSovKCFby%Jn z8eTjAdppoviHi}+Ib3Z#ItVy)5X2hVcvt^b?FH!>89nd+JqYnizVJiE?EQfi;s+^0 z;lR-bWIKOf->_Kd;GrX@{6iN8^Xm#QK}=2StsOIJ6!5Z2hqs5vA4qJt0NBT@tS#lt z#;CC3#!PiCrmd4{1OX>t@}{TfifEMY9s5`fy+L~C*}5ftAMap&Vty!YS;uFhkOJRm z#ND~#oEs;$R4_urGs$$k^deSG;cp!5l9{*x{uDs_8%@mYGE-BLT}_=bg+9`9^X3;c z7`s|(ldrtulTetjGIQzNz=IFGPISXBg+`5ruD|(6@)74Ha1}GS(|B+%TXuV&qMGTC zHB^@@%ULESDEdZ^Y~SnJa}c-O4>^?MH=`! zXnJfc0J^S1@VvzDVUU4rr6&TVWtAizN|A}trJ%?_nl3wDo*a+49fgTMToQYKRcs{@ zo|>8|YZol3oR>K{Jz;BdAWgPSn!&~prR*fQZJ~Gf@JLIW&*!Cm8j>|rY^_mX6xh2f6TrYMk~*WHa`S@>}In$fKTV3}bI&|OehC^{w2PIYy`@Xrcajy7?+S=NcW7pBeBD%M+hGo)I z4(IuKTue>9SzTQXe1Zxmpli{XS%KYNU@!pQQZpJRPKZcl@J2NH@g}*csY}#<9wwiN zpoochLMikE&$QM+y)3t=FHLarwzjV~m^H8^jXN2SpvT8+qcip zw#Q*j;JRi?Kw*L8RFLZX^NoE23!ux_*OM0i>)V4oT0#;*J^$iyI`ubLHoYtDf1Bf`VywFjBC@x)Eijb=0 z*HK9=r@WMYRw}!WP6d&h%sX$^tk@e=? zySr%hU6aerMH`B+m&+jn>F3~dEX?2VkE^NiE zKgRd99Z9LI_Ya-#^rgqg4hsGUdq-MB*3H3@Q-d`owcoKkW<++jR3EE8ZS$8e^{V^# z8<)_ZHh=<^EQX|+zRC$8DgT=#ODHL)FYd#aemi_guSx+!N>p4l53YcLL@bny@T6XM zp>e;XJ3!5htL3q7VX?!>4i_%0h*SRRCSk2qD&4ofe8u8~9(#9Ao?y{OwRCrP1D~jr z{kQRc(tb}G<$}WXf_?XV`5@pmuPwe5u;B3TU+h@`8bnZ+sQoSu~)eC zZWa?@`@bA@%JfcWTU{N8A^H3J-!)~CBE~4ab!*Vjp)Z0KoKsX#cn=?1e9bJ`_6pIg z!)~gkrX~g-G;Gef^vOi~l~)G-o{-?QU;!9*BV{oVNmkat-;_K(#ZVH9?;%-%{rl*7 zgLZ4m{#rDf!#cjm2BCULq#!H;142!#NnXQ^1uX{pS)IFmu6L8YKDi7JZR}5X4G#Y8 z#0d>mRUbZ&w$(9&)6P({j7~IFq?C~vgu^$egpP*S#G$g{8#j1N296gu5z-N*G#ut# zYB6sQuypr;C;=E_jPC*Sgn)^-8R7t>g^Bc>?Ps^LUESR?;MRisD}p3MZE-U7x|UE< ztTT{;BA4j59Xb}Q3U%{UC^547m!QSWI}kFMTs@-OX#b<=?N zw<_U_N~BT-sA>A1gAkP8NHE_eawJJ_gnS|ajiC%Nd(XThqY~3UuvF7natNvqb(YFJ zE{gfl`~eNYpcFkl0X#64I=7Iad4>*)@!NwULO>4-(d2nmIIy#J0BzaqKSk`7o#HWE zzdny&!x@W%toQ;a1HReR+&oZTo_tK9oX=c(DCy+G5~Pq_W{|JH5P!CR|!LwjK(Tsw6H z*I&1{-@JQwTg90nUj_Wtr^zK;zWngXldIHV0re)EuBW*@kR;q#FfOy`*#m{G)L~@Q386{qKSrSZ7 z04r;#tIxsgMwm^Qm{9P%(a4+usmhAaw%=G-4wOS?IUW-e8bzHCQ&{d}BMI6fWP;_o zoFOv`4M~3f2O+pqR0Q>Ak1=XsNP`Y^u{A%h8^@Ps5RyM~a)59UrVHY6twHstS1C1n z#Qj_0W@~4M+u_r;HjUAv&!(j4%j%i&ZO_*``Ulx?!QA zmv?uZqV%ACr2dv&0*WEn?Pd6u#uyAOb<4WXlRm`mS(NkWuJL^@(6E~v6BTudVoWBp zr!QvZ(WF!5B^*caqNc_%E5a`x=niF&Vz97fr_={ zB<1`Y@BuQ~Or|^cwztViQg^{}NE#wJmo5)190tD0XoBI48OxV01>uM??!tA6sZh#M zSS~GX?aqe#s>b}bM01*dFv zP!iQ=Fj^;0+@ewf1Bb2Q<_iufdm%DL;ae zgo$tYxaJE<(?A9f^PKS&rfLcBI}sip4w@A1Yw-+X@YF)Zj_loOPbr3_x9IrIm4LF+ zY|}8&Kgn6BDk@s{7@<~{-vFAT7tH}w)i-X?a4jgj#~pKAy_#Cm)bUT!2~;X^toT-W z`HH~6j~sy#z4UowV+}o)cNuUCKN^0X)&`LQ*eXB{?x^Tgfwd1Du%c;Mn(Rk2hhCfn zcFxf!AfRArG7?Qejny){w-r#HIU3HVps^hsypte@Mu4e7bERUYW(+qO)(9#s`p&1r zBQmqH-WGK+Yd&Y-=T*aeCYqNyg$FIQM_Avs&t+~dB{`XR93dA-t0QdxjM>o*bO&m=N1;_5!P1snAW(A#sxe}B2-E47?_cS$S>ys*f zZ9~8zpeaX8%P{FvOZvfzDJkIX;bX@dL{3FA+)Emy1!q9ZuiIZyvb%NXkZ}^TUC=!w zm7X7SK~V&sD(>{noAl*OmK@@Q2Mw!3M~<9{ba3|Ln4(|&Y(0-~HDO^Ej*hXK3Mf$^ zjt|UyGR4S9`DM7x(l;5)Y=FdhI|{o>{i=gu=Ya}}-q%OaZqslv-tgl2bNDsW4Gu~n zbeHMtg&J5%zT-v;pf--&^HFLc3I=tB9qiB4N4kA0_M1m{+!rH{u(2SJxUz{6JwkWW zl0lMkEijFs)}7woj5p{53o%`jRz{CTuSgnY4o`+B1My7wrLl8VUqYo)DO6{@27S20c!rGSU11kVbA*C?kJKQkx3Zs@%M*#vxOQjWy7lNJ!YZ{~S zQ&GXPn-<_B-lW5~wEUgFYRr0GS{fl&`AZ16zG1B8v^Ay{+EH|DjW}Yk6C=XHZ0b4( zNKS4PI)alX)De#u-+&tWmIE;_1+p20JwxwCD2QN96p&_d?}VQNCs@siwNsL323ZI% z=lZXVgCKUGL?IrbC!{=sbq)^=U6D&Q-O724!>6rLdGh25B@sQ`a2>)yEeA4TVX;#8 z71@be+Ez_F{J;T+BGXSQLr4Avk^~XkWWlo0Sf{GN@?+Zo@0@SED%nQlAgIi|3f&ZcWkl&j?@*qM9W=khize2FCj+a4UU4SFl}MxJ&MgEf#e+?Lz<^Ar z!qs;+l7#qcoZ*k9;Kq~@Y#|=;1BxI^>TUWxLJ6L%dr<@N7TE0JPmF%!#MO0VI)=xa zp%WO<2RyyZuOW(h`RbMM;ny*lye>~Tt?-8lbiNpZtt)y2Ufmg8oV$BZ)tVh5D?+_t zA`8;h#+)yCXewB!M-5~%#zjmty`(24xC2f`ud_yPn?O#44nBzk}-F#`1mo(&So_ zJw?9Jj?HpJHNU2ms(z9nKj1(^@(f@s1y7lK5|z&~D=U=~j?m~9afYDg>2&I1ela3q z#0eSy>L5Vgb{(!b2DjFEiSG&hl_>{V(8E)H?I&qhzYH5j(MetM@o>qvRYzzg0Q|C< sgr&L3%6({E!wIsI5HT)rT4-Vee50~8TP00000 diff --git a/benchmarks/performance/throughput/assets/throughput.png b/benchmarks/performance/throughput/assets/throughput.png index 8617dba36c0d7881055dc49447302184e1931776..fafb0116eec3ba66a3a34199eebe22585e074181 100644 GIT binary patch literal 36881 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 E1>f?}^8f$< literal 26754 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 z⁡~Bcty{*V?9e0z9wzNu(yn*cK6i^_?rH*VZG=b^7$ddg+|2d2`>+8U9~ z+AS@J8UXNENy*LKG_*cb-{sQ2*IQ)S6wSxeTwYn4H;F{B`~2?RJCy`^k&pLcgFTPR zg&mfQ;bjO9E@k>+lCx60I=dRnE-&YPdZK->A@Ol|cpFk(Z1bmqVU*$3k@gM_JUUhD zNs8T+-lXyFm$)iW2I^Gc$31j(ta=;XUi!G+TwxshvCd^8Dzl^SGou&4#L*F})27uC zYRT%-(p4c%lK%TSyfxn#l|MD`bX{JUUYs4{)Gu%WY%TpcV}*#nFf+pb<&>PXG@YQG zk%L2KPR{IJn}SQ)9jJ|oY5x9QH&PxIbP-^5W12zdaGeInnmrbPYc*uOoKg#*G_i$2xb5 zdrXwee-jwk`dj+*AId`K_zg=RQ0-#FD%WX!q+It>=@8Q= z4C&**KyMmAlAcmOog$B1hsNIW$IM@nmXRRH4-y|@=ja&BZMwR$OwAw^+dPE*`Eb7o zoAm~6pips*^J_^#hjaPG{~;N(R+LUrj0Lx%PC4FQVB2+bs?y>8p~FXx(B*&+v46b% zSxih1>T%ipl24!Z^h?Y5*1vuG*8BEtKC`c43y~e)ekZ3DaYxdbnVRN+12C`5M@*Kv z7MmC{Gc#jSa(;x~uk_o&xMv1l%W5)O;1=ovG!PqcTdLzefyxn?17(Y2#l^N=;`Li5 zwygh{5bbG^l8T(`@75BxWe*@P$`xL7293dj(a}+pwyzfu>W(njm)zgwFJ$sO_JkVK zr((p{o3r$tVK?1JXGhvJC{KVz$Z*Q=8{D#90`T>LLW|22WEf<7b&IL-@qi9)gtjl? z{Gvran6JndTetC<8`;%Fi6~z%GK$S)OVLh!LTfNRH6@H{8cQHX<`g(3lx%$45*R@5aZrK&4L2@A5b4^5P;Bi;8uXH0sRw0`p*qCz(J z&$=n(JUZOhr#`Nq*#4IBSz6i{%79I;xY>N(fv7>+K!u{x)zt-5#tZ&{Nb0W7A4NxZ z#$9;&ZQC9_dZdIE$Ru$)h+X4KMGz}iGGAf*6?{>|ZA!pxYQS$Vf5j7yHjoL3 z>E}GBEzGp(}*9uq!1XPXifqHoU1M+s}S-YfbcTZT=E zIIP)P(b<4R*9Lm0zVHYN>2zdIlsm{T^!<^bOh!SIAY`CWzpScCP)%#st|bV9;o+sZ zn+DgW8&LwBHgOFQ6{%T}ssDk?K#<~qMJpsToU<{mq zIy+)O8s)uxJKUC2Q(i8;pjtfBY6*T9r1dvV4b~%e2|E62NGn>9 z8Pz6{(ht01`iCT3hcLGtgbiN=z*;Vf_GDz=^r4a++qO+0&Ikz!$u#S)C5^8&tY9Ji zcofEOvw!bi_uh(N{!S13Pj|N=#eeo^TcJkR3=_HBO~<4W5wJv1Ei4ab z)POQU+U*}3<8)Rcku+(6XU5M1-%=y`Y~HN98i1!ULm;0*Wm2i9xABz6Tzcl)IGMOG zRDU2edS6kJNWMMv;-q(v8ALsoKnM*E4kE0-LNR#KW_|kQlKZi-v7m>}FX{+u;6XC% z`{kLL6UbGn8E*f>AsT|~y)ORx||_z8z5)YjXV-dZWld;&;WmRM+2nPn96(GJX+M1sG;)D=XPH6g*hv33GEU zV9c04fvB@=OmYM+N13hjoQ?RVA~(C91g!NwnCT}e*EkAnRz*pOWCUlED=8^ig?cF;xI(ntG{^Q3( ze0)tpYQzX%57c6%LCs_f<^zGl*4}=HxW`=A9neh!b#cBjaoj_I7*u?xcO_Tdpdh~B zCCpCoA&HvKW-Evytm&u6^(LpM<<6hy8VL;v8R+jfZRK{GGBcM86E`+B-9SM>#~ew& z`L_2@o=w8*f*yP?5SCjRI=?2lys|#oT9r*HMO1K2jbvK$rsvAZ^K)m`ufKIBjsM%X z^hKVZ@6)_$1aY2VJ4x!*OS_I7q)gDn@n zVJt=a%61%;Evu^HSuY+eqt@D95~x5?vOPNF!`^>#568#m3Z0r@-)1y%;83Mxs#AJ% zg&t)P3o}2z$jP?UpvT{R#GcPhj|rN_89We|kZ8-XVcfp`GS{BK$B#wle?FG8Z4&6cr9Zs zDZp=gEKHfBS~3r?MILNo-AK&(&T1JI$wrH5l=Q_5zmh{j_So3in3|gQ=yBqzgY^kx zoyBR57J9mSMqXTs}(#v~a`-m3Ue6&Z1@TL)IQv zvNuP~VkvSIW@j@kl^b(eL^hJQsGT^uZgt z1<25FFb)^6oxEv?8o55+Zzm-Zhmh0oKu=G7j*UV6N6E9Mp1FLjcE&l$J(A?8aQ$*F z>JXdaWv8EIkHGWNZC-D<2r|J2U>2OVySqEnS!LxqfT1`sv!JM`4g_AX3)skKk{c_O zRvus7LXH|ctLy9SfrC(Saand|S!xi^#{=yAJ@@xS&u1*H*fZyDy$=WuZY*?n-mu{g zI3AR{`k7BD0xspj~_pth>wYhX<+eIOjJE-QuWq(Oy9`pXSCM}t7WLd-$zX7>FqsIB6@Es z3w#leaVBsnvqDq^kGV2-KE{j9GEPD4&uAp56nVJT$2#{`?9$QEG4Fm!(+vs?Bo}lm ztELn!6q!4A?C4FN+W03Pj5`2$*aAcl$`L=Ii6kg6ZrHG){xn-KxNJ34J}8812OYT8 z7XCTLwvJdQK5fNbtFxV6oZffu-zNwXP0f(UkLv@ET=>KfT&xQ<5CfZ>~|M*De#?U?um9dKO^& zf}g*YmX_AA@`b;X$Gq;7i#KrYKN~hM3fLYvdbG5&)8HZVgutH+z@qDSXTPIO&YdcH zaD>+~o-9yn#lP$BMsC7Okwuc6H{K;#1lfn2coxz7aBM7*91_6@ z0jFi>%+t)oM9XVKDFg=}8UTl1z-!3yOblJX$sDqx^rh7^C9xK3>w7^ZQ08=jN6Tjj5J>)6s6;O1ZM)OP!9m= zFWXAfjjWE+cLg6nec-md;3y^aS>g2jS+~2tmQxpl(4lglt2}gwVH+tj8VqhG?jUv3i3n-U?)iwh zxjCW(Z#FnaS8$P7DslAn6PXDeLUEK)m(uw%|e0LKQ6K!qn_fCl!4)(~vfV@be zfjr)G!hZmYSXZXD^_45HCgOQm8=9K3zdEI=v!_bfqpZXo8JU_=({X>Yx>VP{K_`~%gAU`K*z{9*41^%#>OCBkFu-3pShi=<3KGV$`>HiA#PVHxhVaCHP393J0SY< zc5L3VCAevmbzN*Z0O^AmFK}!SQK2)%`){G7)Wm>*M7VkLW{Up5-(_hBqOK7^A!qab z?NvZ6pfF0Nj%%~+JDBCMcvu7lT~Q0-ulumOXfA_pb#-+`xsD7iVE3~OQ4+Xab37d~ z%j&{Vnv@^CaTr@P`=7y*$pPM$r*?I5A%q8T+F@LmZqmHKOd<&qq&+tB>6N)2MA&Te zdP&NzfdQ8v?`|ONEi5h)RW;y07Eh)F3nL>V0ka-qmo9@}SQ#={y?^iCqp^9BK*>=8 z3zEc4qJ>ol^iahJxzD8Nr)ebr-jbk(xB^L;q74!A%R?lTgPVeMx%U7n19~uXUAU%T zwVH78Pk2p{vF?v$)hoaG*?EHXKlgaHoqgKX{N7jMT*fje336DReJ9_||!etg() zja+iB>U7T2*Th>sh)-P3i=Hz}9L~nao(0_^PwP9X5b&6I{Rb+&4DgMYa3nDA3I?t7 z+N>U-5CW29W6TGD)c1p3GOs~t>xgm`Q-Ku|^Lj$EA~N9R<0tHCFOm)NpX#&A{Eb9A z)x^a=fS*RT-MN2%R9KjWmH++wLeRHWRfDt+Ed5GOJ8n?41Xv%&r+?w0zKcm@?|rLT z<&Ou~i^o48hEGh%y0ZxT=oc7V9!rfxRcwfd;N9G&UcR0{M#rAO3QBhiJ@*G%Uw`4Y zIFdtTUmcyco6dBbZv(EQL~8i<#;WB_(Y3|CpFcHoN)!GjnHO(fzjkfVTYu{k5{G3| zinM~lZt($!JmLY%0+`$iJ1%l*=2X4-6F&e)`&un@}XVP|uH zi9bne`$>lZMf0Wspd$$Ly~q3iCMU(qkkC9p`w>Hi*zxt*8M~*y|M|OhPaw}?5h8Wy zVk>xfc>Kf{zuh4x*ZUTLFkpRZs;gd!m=gj!+c($Fmn4`RFbF%|Cd&q|&Wy7BeJ|HJma?)6}pebIjOw#e^~ zzu&w$nW-JDJzXVb>v=6U-E}7U05RqKxX;x+EsvN6nY}owx9#7VMoPGSOO3eu@wJ!! zgoyv{%Jko7r{jw+M(o8Fz?udj9Q$N^eEd(z2dJx&q*}gkgj}$#)pGY92VDYD>Do|g z&XikW1F?fqlpvzpAmac!wCCC<1YaOWS>rb|lwDwxU<|QGP9HQurGk>)Yh}J4UXD$K zqA%h!{0%hF-VM~m1xU1)8^!i)-;SO71pmvj>D0FX2)U!3>e=JR9~%zXungrK)J;qJ zB?(K~+b=5f-s(k%E>%(U$C{c52nzrH*f?{1CMyp^N&H~Q8~p>aa&io(J?0cdmw%!J zwR&*>ev;zzo07ks>EP2ie*8GHXp1$EiE0~+>%9ibRQ+RU9HP_Y0%b>*m2&V?8%^Izi zJlVbB;-dl6&7y_+H3`ixQg%{)en1CH&UndggFT17VpypZC_8!bw~`cJ<>TAMMMLFFHi>5kws4DXD(o1)tD)Q^HunPeBseeXdHaK?=Xxzs z&$^n!@0n?8&ZhBG21kuu3^*61Z{zZ5R!vyLX!XR?gy#OiO=9%ka0wu^n7qFgy642i7(PBKC(FprA-2E*{)o3Qar=js z+751o4@sHX*^{(C2A*+D1y?C28t;oyN>=48U0jT?Pk-#{{Z&7ib;_^Fl4o+o=WF?U zed8M3TiM_8nt~{=n=iVY@jT=9+MLJ7uUhNT<6DIphD^M%$*SBY;(N2UMC<#$Q9gj0 zCvaj&balxdE(k`?x#wR#G7YX-x4r<@EBn4GXerR>tFVLtXH!*n5i6D@G;5Y=Ct+QH z(@DKFa{aBFH%+QSPFT0{po+UktyO%O_?Y4Wy0w?Y#NHva*twl)i#r&b*fp1oAztWbHbMR zY~SBP<$o11_HX$4Zw$TvgS1>-nI49Dl_Z!WZyce&I80shMI}*HDBN`QREkQ^bNc9C zHA}XC@z#%WyB#sy)mAKhXB*Fo z3n2%B%d~6$!!xg;XZnPo09lW(mx-F1I%7Y`Lx%ke-M*YFi=C@PM1;!@ez@1NxcK!| z>1xksmG$M0PQfz0y+4jOx4>6At8u_%{KtC$yl`I2nYmYRgn$u4QC^AfT}qwAv-d0& zONj2fd-_mw3xm~&f8lb5E^yz@PJzYBPfiF$cFU-68Grd&8E{I%b zut@i{$q#oF<|B{8Ss;G*;~i?K;jalUt@25GwspAdE^;zIcF^EJRb}O-&6`tH5+3r1 z3n0p2SRlY#;|5Xo)ejj2_&5+7h19o5qmK$1z5t|7TRXe^4;~1^S^&l&4>;q+IbW32 z(D5K2$rmY;6aNv`PRdJmc5jlClZ%UsX)c3Ef^Y|%5zNQr;NVc;wOZuiNWKpl%_1=n zc8}qjWn|>^24gN!`Gf-`v^NdZ%$l+?=t4nh^2BY3ZLH)s&V)b!VJijRIh1=dm_t}V z&7ar8#t6gkz4;d;#h2$yn@#&GvbX8F?v5^6WBpU;6xOO#eMmb&oxZ)=gC(#Go~sZX zqWIlSyN~aK)(VCSl@Ir>jij2a#%=j^4s>>rg^z4Fh|TW8=SA`1?OOp%y18*J*1%|M zR%NCX2FdX9Wmx-uvTw)j)kFq&o$@S7*D786#9`^Dan2iVT2cXNh01#q?XeJk8+|-i zo2V*OmHPcN)a4rZVN#ToR&L(U6dt-wWhc{F=@oS?m_9)O^8#%SvZW^UWC9opc%0x3 z)dJZ8`r&&od4P|dJLHX#**6#9Q1au|9YD6=&`>R~Yv$k5u|2S^>@+s*Jl=O^saU+P zLQ||b$$EqO7>gnC<##8!3{>N1Bv4>iS5yeOj6d7C6$Q6Ws=E8q+yuCwGF9er0l|gu z;jGR^#|}t0Z9GYQE8_&UwC}LU#rhm$v z?Z3BufUN*FcF2xj+u9am{7;98dU*niJmEC(gmD0VNzvctH*ej#Ms~~|^`nAaK1DZo zGNb0S@56^%si+np0)WzG_l6&FD*e(&NF_gi{7@WDAoJud5gv!UA&cP?ZRwY8!Q%@g z>@T_^U|bP&Na`W@#x$Lr;~XO`X@T?*L_$xR#)2E>+OxS%ofaY?sJKUU&uCtNNde;| z^>80+$7Awd5Y{T%_92|#rnR-z34SZe*Pw2zs;ZnjuW3-$!ffp)xntKZz>?JNh-0vM zV-_HN>ch_oGS!;~TrxaXrLbCU#WY}YII6WUZ~BK_H#Ya|c4F&0bJ=%7v!8a=?uy9s z5DJpZ>E2E7<299*mNqr1Q@#eZ1i=VpePvx9Cx{+CKE4v+7FYo8Y^Dng2@wEQi@pOr z%12}w$*T1;j7zMni6P9mcbBVsyw5~4Tdo;i-CB@7N$~-0B=7==I&jBYKx0H0dfK`= z*F&A9trv05_%OJ-2Y!BXGBR#ZG{935^b=Nm0%WxrX*oF#mG}qdl~C=&!on6_8b*n@ zH%-OAzj$6=j?*Nv=2ptEmFuSS(v|1!$~eA7B|QBtbyGi0LB>@vU8MKS?R~vV3hLRf zzJ6GFURQUrm*c>pllfnpPx7CVdT6)8EI56mOnsLXWOl&wobTVi?;h-Vn)vv4$xe_G zf<8r2uA$ssI-UVhp-P&U1ao~n(z5s8>N>YDw>&M`FXAuSx7ltrq_V26cc;3hNwz%k zle`>T9n1a*c-YVKr>?L~S zNF2T$@6P}#L%-wDD%%XS*6T(x28QO}sUI5imx00-tar&KGbtd*jF*o-9OIHu zTfQ_=|BZH~MzF0~y+ic8fPh(+&#w5|oz5%rCkoFqiVkk@OEKkKY7Qx|I_0{0;8H@C zM-NPP=)Bj zdC?yD$zjYSQVe)72WRKq;*cf@S{tza(pCAB=rMr{;r5+7Z7s{w)p{^#Qg)4u=)pDy zR~V{QEO$N1iAv65RU|tEjd*{;m0$HJZe5?dOi5Jhg0N!p&!rnvt>WWvgn`NKRNBu z;4j$XrKgl^5MZ!MHJ>jJQ*|6CN^R}bSDLT8vBb5j>SaT?E@q&y_3`A%lSt*nLzXRm7R1&jDN_C(n1(|grGD{b1v|DqyegoQnzcO@n!zIycvy!;}}%(3}^J~`&~N1=Ja^$B`l28xpav6&$p zKANZOf<{*e%z%90FxJstQUb`;i;#o;+5y37zFt9eSHCQNaCM=%n5rOTWzGGqQ}ZMU&Pji+AuNQY>@xv^F1douUc~B`hWh>%;6({odTD^X6f`?94UKr2rVEc zR#4ra^!dJ-VO*P8$3f1Seu%y8~A`#6rl)amv!>ij=U^T$&alC$b~Q3=T|9PPU@l z^(Y~}NlN+b!6;sybq99FYaSi|3w>i_Hq#%-?>#ZzPZ@W_2*KUlN-4P!+9*t4OyOi@ z>z(f>VVd>p*Grx~TNTEe40Zz3dGFpmVCBUzuT@J+%Y38q?Vm>9?$nfg)MdEz(4%1;^?XQHD31Flox zG6DV{(vAk)(_m21Thj_>bNt_5RVpi;LQcbS%iZ^Pbfq|)$yX$~()`zb9Sxny56~`f zj4)2=A*o&gVhK(n?tnx)>wI7z*F_NI4L$ksadCoh(8jS5?_4igW}u9Wh=6nh_BS8= zSAna2R$f42ED)7CCpGbzi>zK7J`kh=J^`W^S$uk8f=<}Q_Tt5h&_H{S|NANbe_Gqx zsP}Lk5D=Kdwgy~m18t0i2p8)DOmzSqWqM>f;n~kng)P&wv(bFk+Go#xf&s+9wKo`^ z4s?CND8zO6@Y#QdWTMQP86-9auKVT7mzO@?hL-pWqX{E(`W0*9{z#kZgnvDR*NsCj z&jCso{56rMocHrtL3Vxo@6HNvJ@CNSR|ldUwkFYar=Gfie1l*4*4(TSk3s~mCzH+x zs=j<_2g`v^5xqq=Hl1k(#nA}N*ve1yg^5pHk6U7(G;klu0?UZ13C6A*Fj95HU%!3R ztbYYk4vlQ_e|O99m{>?qP;C9+%xNCNGx+c5=ltg{?LMX%5EZpD_X+o))6HoK)zjL{Zaxae=7rH}Ga7Dw*eL@cs znxI9)O9D`&>NMZ~TMLOF@-{32nY87kfyBb4Kgbn7aMV4Iy z*_pv|iM~g}B5NV_&h{NUVDEE

+-S@?J0`-bkZxUf*6O^#yEyv?cjAfh=8e^YvR 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 From b4dd5393f2f17c374a993b70090c429c5bafd235 Mon Sep 17 00:00:00 2001 From: mkbeh Date: Sat, 19 Sep 2026 13:01:48 +0300 Subject: [PATCH 8/8] docs: clarify cleanup lifecycle --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 72cf9d0..f0fed01 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,9 @@ cache, _ := pacecache.New[string, string]( // Start automatic deletion of expired items. go cache.StartCleanup() -defer cache.StopCleanup() + +// Stop automatic deletion of expired items. +cache.StopCleanup() ```