crema

package module
v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 31, 2026 License: MIT Imports: 14 Imported by: 4

README

crema ☕️

A Go cache library with probabilistic revalidation and optional singleflight loading. It smooths refreshes near TTL expiry while deduplicating concurrent loads.

crema logo

Features

  • Smooth probabilistic revalidation near expiry
  • Built-in singleflight loader (can be disabled)
  • Zero external dependencies in the core module
  • Pluggable storage (CacheProvider) and storage codecs (CacheStorageCodec)

Core functionality is covered by a high level of automated tests.

Revalidation Algorithm

Within the revalidation window, the cache reloads with probability

p(t)=1-e^{-k(w-t)}

where t is the remaining time and w the configured revalidation window. The steepness k is set so that $p(0)=0.999$, so the probability is $0$ when an entry enters the window and approaches $0.999$ as expiry nears. This avoids a fixed refresh point and helps smooth spikes near expiry.

Revalidation curve

How widely reloads actually spread depends on how often the key is requested: because the probability is drawn per request, frequently requested keys tend to reload earlier in the window than rarely requested ones.

This design is inspired by the following references:

Installation

go get github.com/abema/crema

Providers and codecs under ext/ are separate Go modules. Add only the modules your application uses. For example:

go get github.com/abema/crema/ext/golang-lru

Go 1.25 or newer is required.

Quick Start

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/abema/crema"
	golanglru "github.com/abema/crema/ext/golang-lru"
)

func main() {
	provider := golanglru.NewCacheProvider[crema.CacheObject[int]](128, time.Minute)
	cache := crema.NewCache(provider, crema.NoopCacheStorageCodec[int]{})

	value, err := cache.GetOrLoad(
		context.Background(),
		"answer",
		time.Minute,
		func(context.Context) (int, error) {
			// Database or computation logic here.
			return 42, nil
		},
	)
	if err != nil {
		panic(err)
	}

	fmt.Println(value)
}

Usage Notes

  • CacheProvider: Responsible for persistence with TTL handling. Works with Redis/Memcached, files, or databases.
  • CacheStorageCodec: Encodes/decodes cached objects. Swap in JSON, protobuf, or your own codec.
  • CacheObject: A thin wrapper holding Value and absolute expiry (ExpireAtMillis).
  • Revalidation fallback: Revalidation failures return a still-valid cached value by default. Missing or expired values still return the loader error.

Options

  • WithRevalidationWindow(duration): Set how long before expiry probabilistic reloads may start (0 disables them)
  • WithDirectLoader(): Disable singleflight and call loaders directly
  • WithMaxLoadTimeout(duration): Set max duration for singleflight loaders and their synchronous cache writebacks (ignored with WithDirectLoader())
  • WithRevalidationFallback(enabled): Enable or disable revalidation fallback (enabled by default)
  • WithLogger(logger): Override warning logger for get/set failures
  • WithMetricsProvider(metrics): Record cache and loader events
  • WithNegativeCacheProvider(provider, ttl, isNegative): Cache absent loader results through a separate provider
  • WithLoadErrorCacheProvider(provider, ttl, shouldCache): Cache selected load errors through a separate provider

The default singleflight load path runs in an internal goroutine. crema does not recover panics from that path, so application-provided loaders, callbacks, metrics providers, and cache providers must avoid panics or recover them at their own boundary. WithDirectLoader runs the loader in the caller's goroutine.

Negative Cache

Load results are not cached by default. Use WithNegativeCacheProvider when a loader result means that the value does not exist. This includes a successful empty result and errors such as sql.ErrNoRows or an application-specific ErrNotFound.

cache := crema.NewCache(
	provider,
	crema.NoopCacheStorageCodec[int]{},
	crema.WithNegativeCacheProvider(negativeProvider, 500*time.Millisecond, func(value int, err error) bool {
		return value == 0 && err == nil || errors.Is(err, ErrNotFound)
	}),
)
  • Get does not use this cache. After a value-cache miss, GetOrLoad returns a live cached negative result without invoking the loader.
  • The negative provider is a CacheProvider[CacheLoadResult[V]]; its TTL is independent of the value TTL passed to GetOrLoad.
  • A still-valid value wins over a negative result during revalidation.
  • Successful Set and Delete invalidate the negative result.
  • An in-process provider such as Ristretto preserves error identity for errors.Is and errors.As.
  • A NegativeCacheMetricsProvider can record negative-cache hits and stores.

WithLoadErrorCacheProvider is for errors that do not mean absence, such as a short-lived rate-limit or backend-unavailable error. Its provider is a CacheProvider[error]. It can be configured together with the negative cache; negative results take precedence when both predicates match.

Implementations

CacheProvider
Name Package Notes Example
RistrettoCacheProvider github.com/abema/crema/ext/ristretto dgraph-io/ristretto/v2 backend with TTL support. ✅
RedisCacheProvider github.com/abema/crema/ext/rueidis Redis backend using rueidis. ✅
ValkeyCacheProvider github.com/abema/crema/ext/valkey-go Valkey (Redis protocol) backend. ✅
MemcachedCacheProvider github.com/abema/crema/ext/gomemcache Memcached backend with long-TTL handling. -
CacheProvider github.com/abema/crema/ext/golang-lru hashicorp/golang-lru backend with a provider-wide TTL. Quick Start
Provider github.com/abema/crema/ext/madvfree Best-effort anonymous-mmap byte cache for 64-bit Linux and macOS. ✅
CacheStorageCodec
Name Package Notes Example
NoopCacheStorageCodec github.com/abema/crema Pass-through codec for in-memory cache objects. Quick Start
JSONByteStringCodec github.com/abema/crema Standard library JSON encoding to []byte. ✅
JSONByteStringCodec github.com/abema/crema/ext/go-json goccy/go-json encoding to []byte. -
ProtobufCodec github.com/abema/crema/ext/protobuf Protobuf encoding to []byte. ✅
BinaryCompressionCodec github.com/abema/crema Wraps another codec and zlib-compresses encoded bytes above a threshold. ✅

NewBinaryCompressionCodec does not limit decompressed size by default. Use WithMaxDecompressedBytes when compressed values may come from an untrusted or shared backend.

MetricsProvider
Name Package Notes Example
BaseMetricsProvider github.com/abema/crema Embeddable no-op base for custom metrics providers. -
NoopMetricsProvider github.com/abema/crema Default metrics provider; records nothing. -

MetricsProvider records cache operations and loader starts, reasons (miss, expired, revalidation, or get_error), errors, and concurrency. Direct loads always report a concurrency of 1. Embed BaseMetricsProvider and override only the callbacks you need.

Concurrency

Cache is goroutine-safe as long as its CacheProvider and CacheStorageCodec implementations are goroutine-safe.

Development

go generate
go test ./... $(find ./ext/ -type d -mindepth 1 -maxdepth 1 | sed 's|$|/...|') ./ext/madvfree/bench/...

Examples that use Redis or Valkey require a server on 127.0.0.1:6379; CI runs them separately with the required service.

Tools

  • cmd/plot-revalidation: SVG plot generator for revalidation curves

Why "crema"?

Crema is the golden foam that forms on top of a freshly pulled espresso coffee shot. Like crema that gradually dissipates over time, this cache library probabilistically refreshes entries, ensuring your data stays fresh without the overhead of deterministic expiration checks.

Documentation

Overview

Package crema provides a probabilistic cache with revalidation and loaders.

The cache can deduplicate concurrent loads via singleflight. Use WithMaxLoadTimeout to cap the execution time of singleflight loaders and their synchronous cache writebacks. When WithDirectLoader is used, the max load timeout is ignored and loaders run with the caller context.

The default singleflight load path runs in an internal goroutine and crema does not recover panics. Loaders, callbacks, metrics providers, and cache providers supplied by the application must therefore avoid panics or recover them at their own boundary. WithDirectLoader runs the loader in the caller's goroutine.

By default a still-valid cached value is served when a revalidation load fails, instead of propagating the loader error. Use WithRevalidationFallback(false) to always propagate loader errors.

Load results are not cached by default. WithNegativeCacheProvider caches results that mean a value is absent, while WithLoadErrorCacheProvider caches selected errors that do not mean absence. Both use an independent TTL.

Index

Examples

Constants

View Source
const (
	// DefaultCompressThresholdBytes is the default threshold size
	// above which values are compressed in BinaryCompressionCodec.
	DefaultCompressThresholdBytes = 1024 * 2 // 2 KiB

	CompressionTypeIDNone byte = 0x00
	CompressionTypeIDZlib byte = 0x01
)

Variables

View Source
var (
	ErrDecompressZeroLengthData      = errors.New("invalid data for decompression")
	ErrDecompressedSizeLimitExceeded = errors.New("decompressed data exceeds configured limit")
	ErrUnsupportedCompressionTypeID  = errors.New("unsupported compression type ID")
)

Functions

func WithDirectLoader

func WithDirectLoader() func(*cacheConfig)

WithDirectLoader disables singleflight and calls loaders directly.

func WithLoadErrorCacheProvider added in v1.2.0

func WithLoadErrorCacheProvider(
	provider CacheProvider[error],
	ttl time.Duration,
	shouldCache LoadErrorCacheErrorPredicate,
) func(*cacheConfig)

WithLoadErrorCacheProvider caches errors selected by shouldCache through provider.

Example
provider := &testMemoryProvider[int]{items: make(map[string]CacheObject[int])}
negativeProvider := &testLoadErrorProvider{entries: make(map[string]error)}
cache := NewCache(
	provider,
	NoopCacheStorageCodec[int]{},
	WithLoadErrorCacheProvider(negativeProvider, time.Second, func(error) bool { return true }),
)

backendDown := errors.New("backend down")
calls := 0
loader := func(context.Context) (int, error) {
	calls++

	return 0, backendDown
}

for range 3 {
	_, err := cache.GetOrLoad(context.Background(), "answer", time.Minute, loader)
	fmt.Println(errors.Is(err, backendDown))
}
fmt.Println("loader calls:", calls)
Output:
true
true
true
loader calls: 1

func WithLogger

func WithLogger(logger *slog.Logger) func(*cacheConfig)

WithLogger overrides the default logger used for cache warnings.

func WithMaxLoadTimeout

func WithMaxLoadTimeout(duration time.Duration) func(*cacheConfig)

WithMaxLoadTimeout sets the maximum duration allowed for singleflight loader execution, including its synchronous cache writeback. A non-positive duration disables the timeout.

func WithMetricsProvider

func WithMetricsProvider(metrics MetricsProvider) func(*cacheConfig)

WithMetricsProvider overrides the default metrics provider.

func WithRevalidationFallback added in v1.2.0

func WithRevalidationFallback(enabled bool) func(*cacheConfig)

WithRevalidationFallback controls whether GetOrLoad returns a still-valid cached value when revalidation fails. It is enabled by default.

func WithRevalidationWindow

func WithRevalidationWindow(duration time.Duration) func(*cacheConfig)

WithRevalidationWindow sets how long before expiry probabilistic revalidation starts. A zero duration disables probabilistic revalidation.

Types

type BaseMetricsProvider

type BaseMetricsProvider struct{}

BaseMetricsProvider is a no-op metrics implementation.

func (BaseMetricsProvider) RecordCacheDelete

func (BaseMetricsProvider) RecordCacheDelete(context.Context)

func (BaseMetricsProvider) RecordCacheGet

func (BaseMetricsProvider) RecordCacheGet(context.Context)

func (BaseMetricsProvider) RecordCacheHit

func (BaseMetricsProvider) RecordCacheHit(context.Context)

func (BaseMetricsProvider) RecordCacheSet

func (BaseMetricsProvider) RecordCacheSet(context.Context)

func (BaseMetricsProvider) RecordLoad

func (BaseMetricsProvider) RecordLoad(context.Context)

func (BaseMetricsProvider) RecordLoadConcurrency

func (BaseMetricsProvider) RecordLoadConcurrency(context.Context, int)

func (BaseMetricsProvider) RecordLoadError added in v1.2.0

func (BaseMetricsProvider) RecordLoadError(context.Context)

func (BaseMetricsProvider) RecordLoadReason added in v1.2.0

func (BaseMetricsProvider) RecordLoadReason(context.Context, LoadReason)

type BinaryCompressionOption added in v1.2.0

type BinaryCompressionOption func(*binaryCompressionConfig)

BinaryCompressionOption configures a BinaryCompressionCodec.

func WithMaxDecompressedBytes added in v1.2.0

func WithMaxDecompressedBytes(maxBytes int64) BinaryCompressionOption

WithMaxDecompressedBytes limits the size of zlib data after decompression. A non-positive value disables the limit. The default is disabled.

type BufferEncoder added in v1.2.0

type BufferEncoder[V any] interface {
	// EncodeTo appends the encoded cache object to buf.
	EncodeTo(buf *bytes.Buffer, value CacheObject[V]) error
}

BufferEncoder encodes a cache object into a caller-provided buffer. On error, EncodeTo must restore buf's original length.

type BufferReleasePolicy

type BufferReleasePolicy interface {
	CanReleaseBufferOnDecode() bool
}

BufferReleasePolicy declares whether Decode can safely release buffer-backed input.

type Cache

type Cache[V any] interface {
	// Get returns the cached entry for key.
	Get(ctx context.Context, key string) (CacheObject[V], bool, error)
	// Set stores a cached entry for key.
	Set(ctx context.Context, key string, value CacheObject[V]) error
	// Delete removes a cached entry for key.
	Delete(ctx context.Context, key string) error
	// GetOrLoad returns a cached value or uses loader when missing or revalidating.
	GetOrLoad(ctx context.Context, key string, ttl time.Duration, loader CacheLoadFunc[V]) (V, error)
}

Cache coordinates probabilistic revalidation with optional singleflight loading. Implementations are safe for concurrent use as long as CacheProvider and CacheStorageCodec implementations are goroutine-safe. Use NewCache to construct an implementation.

Example
provider := &testMemoryProvider[int]{items: make(map[string]CacheObject[int])}
codec := NoopCacheStorageCodec[int]{}
cache := NewCache(provider, codec)

value, err := cache.GetOrLoad(context.Background(), "answer", time.Minute, func(ctx context.Context) (int, error) {
	// Database or computation logic here
	return 42, nil
})
if err != nil {
	fmt.Println(err)

	return
}

fmt.Println(value)
Output:
42

func NewCache

func NewCache[V any, S any](provider CacheProvider[S], codec CacheStorageCodec[V, S], opts ...CacheOption[V]) Cache[V]

NewCache constructs a Cache with defaults and optional overrides.

type CacheLoadFunc

type CacheLoadFunc[V any] func(ctx context.Context) (V, error)

CacheLoadFunc loads a value when it is missing or needs revalidation.

type CacheLoadResult added in v1.2.0

type CacheLoadResult[V any] struct {
	Value V
	Err   error
}

CacheLoadResult is the value and error returned by a cache loader.

type CacheObject

type CacheObject[V any] struct {
	// Value is the cached value.
	Value V
	// ExpireAtMillis is the absolute expiration time in milliseconds since epoch.
	ExpireAtMillis int64
}

CacheObject wraps a cached value with its absolute expiration time.

type CacheOption

type CacheOption[V any] func(*cacheConfig)

CacheOption[V] configures a Cache for values of type V.

func WithNegativeCacheProvider added in v1.2.0

func WithNegativeCacheProvider[V any](
	provider CacheProvider[CacheLoadResult[V]],
	ttl time.Duration,
	isNegative NegativeCachePredicate[V],
) CacheOption[V]

WithNegativeCacheProvider caches loader results selected by isNegative as absent values. The provider value type V must match the value type used by NewCache.

type CacheProvider

type CacheProvider[S any] interface {
	// Get retrieves a value from the cache by key.
	Get(ctx context.Context, key string) (S, bool, error)
	// Set stores a value in the cache with the specified key.
	Set(ctx context.Context, key string, value S, ttl time.Duration) error
	// Delete removes a value from the cache by key.
	Delete(ctx context.Context, key string) error
}

CacheProvider abstracts storage for encoded cache entries. Implementations must be safe for concurrent use by multiple goroutines.

type CacheStorageCodec

type CacheStorageCodec[V any, S any] interface {
	// Encode returns the cache object encoded into storage value.
	Encode(value CacheObject[V]) (S, error)
	// Decode reads the storage value into a cache object.
	Decode(data S) (CacheObject[V], error)
}

CacheStorageCodec encodes and decodes cache objects to storage values. Implementations must be safe for concurrent use by multiple goroutines.

func NewBinaryCompressionCodec

func NewBinaryCompressionCodec[V any](
	inner CacheStorageCodec[V, []byte],
	compressThresholdBytes int,
	opts ...BinaryCompressionOption,
) CacheStorageCodec[V, []byte]

NewBinaryCompressionCodec returns a codec that conditionally compresses encoded values with zlib when they reach the threshold. A threshold of 0 always compresses, and a negative threshold disables compression.

type JSONByteStringCodec

type JSONByteStringCodec[V any] struct{}

JSONByteStringCodec marshals cache objects as JSON bytes.

func (JSONByteStringCodec[V]) CanReleaseBufferOnDecode

func (j JSONByteStringCodec[V]) CanReleaseBufferOnDecode() bool

func (JSONByteStringCodec[V]) Decode

func (j JSONByteStringCodec[V]) Decode(data []byte) (CacheObject[V], error)

Decode unmarshals JSON bytes into a cache object.

func (JSONByteStringCodec[V]) Encode

func (j JSONByteStringCodec[V]) Encode(value CacheObject[V]) ([]byte, error)

Encode marshals the cache object into JSON bytes without a trailing newline.

func (JSONByteStringCodec[V]) EncodeTo added in v1.2.0

func (j JSONByteStringCodec[V]) EncodeTo(buf *bytes.Buffer, value CacheObject[V]) error

EncodeTo appends the cache object as JSON bytes to buf without a trailing newline.

type LoadErrorCacheErrorPredicate added in v1.2.0

type LoadErrorCacheErrorPredicate func(err error) bool

LoadErrorCacheErrorPredicate reports whether a load error should be cached. Implementations must be safe for concurrent use.

type LoadReason added in v1.2.0

type LoadReason int

LoadReason describes what triggered a load.

const (
	// LoadReasonMiss indicates that no usable cache entry was found.
	LoadReasonMiss LoadReason = iota
	// LoadReasonExpired indicates that the cached entry was already expired.
	LoadReasonExpired
	// LoadReasonRevalidation indicates that the cached entry was still valid but
	// was picked up by probabilistic revalidation.
	LoadReasonRevalidation
	// LoadReasonGetError indicates that reading or decoding the cached entry failed.
	LoadReasonGetError
)

func (LoadReason) String added in v1.2.0

func (r LoadReason) String() string

String returns a stable, metric-friendly name for the reason.

type MetricsProvider

type MetricsProvider interface {
	// RecordCacheHit is called when a cached value is successfully returned.
	RecordCacheHit(ctx context.Context)
	// RecordCacheGet is called when a cache lookup is attempted.
	RecordCacheGet(ctx context.Context)
	// RecordCacheSet is called when a cache write is attempted.
	RecordCacheSet(ctx context.Context)
	// RecordCacheDelete is called when a cache delete is attempted.
	RecordCacheDelete(ctx context.Context)
	// RecordLoad is called when a load is started by the leader.
	RecordLoad(ctx context.Context)
	// RecordLoadReason is called with the reason that triggered the load.
	RecordLoadReason(ctx context.Context, reason LoadReason)
	// RecordLoadError is called once when a loader execution fails.
	RecordLoadError(ctx context.Context)
	// RecordLoadConcurrency is called when a load finishes with the number of
	// callers sharing that loader execution.
	RecordLoadConcurrency(ctx context.Context, concurrency int)
}

MetricsProvider receives cache and loader events for instrumentation. Implementations must be safe for concurrent use and should avoid blocking.

Load latency is intentionally not reported here; wrap your CacheLoadFunc to measure it with your APM.

Embed BaseMetricsProvider to implement only the callbacks you need.

type NegativeCacheMetricsProvider added in v1.2.0

type NegativeCacheMetricsProvider interface {
	MetricsProvider
	RecordNegativeCacheHit(ctx context.Context)
	RecordNegativeCacheSet(ctx context.Context)
}

NegativeCacheMetricsProvider optionally receives negative-cache events.

type NegativeCachePredicate added in v1.2.0

type NegativeCachePredicate[V any] func(value V, err error) bool

NegativeCachePredicate reports whether a loader result represents an absent value. Implementations must be safe for concurrent use.

type NoopCacheProvider added in v0.1.9

type NoopCacheProvider[S any] struct{}

NoopCacheProvider is a cache provider that does nothing. All Get calls return a cache miss, and Set/Delete calls are no-ops. Useful for tests or when caching should be explicitly disabled.

func NewNoopCacheProvider added in v0.1.9

func NewNoopCacheProvider[S any]() *NoopCacheProvider[S]

NewNoopCacheProvider constructs a NoopCacheProvider.

func (*NoopCacheProvider[S]) Delete added in v0.1.9

func (n *NoopCacheProvider[S]) Delete(_ context.Context, _ string) error

Delete does nothing and returns nil.

func (*NoopCacheProvider[S]) Get added in v0.1.9

func (n *NoopCacheProvider[S]) Get(_ context.Context, _ string) (S, bool, error)

Get always returns a cache miss.

func (*NoopCacheProvider[S]) Set added in v0.1.9

func (n *NoopCacheProvider[S]) Set(_ context.Context, _ string, _ S, _ time.Duration) error

Set does nothing and returns nil.

type NoopCacheStorageCodec

type NoopCacheStorageCodec[V any] struct{}

NoopCacheStorageCodec passes CacheObject values through without encoding.

func (NoopCacheStorageCodec[V]) Decode

func (n NoopCacheStorageCodec[V]) Decode(data CacheObject[V]) (CacheObject[V], error)

Decode copies the cache object.

func (NoopCacheStorageCodec[V]) Encode

func (n NoopCacheStorageCodec[V]) Encode(value CacheObject[V]) (CacheObject[V], error)

Encode copies the cache object.

type NoopMetricsProvider

type NoopMetricsProvider struct {
	BaseMetricsProvider
}

Directories

Path Synopsis
cmd
plot-revalidation command
Command plot-revalidation generates an SVG plot of the revalidation probability curves.
Command plot-revalidation generates an SVG plot of the revalidation probability curves.
ext
go-json module
golang-lru module
gomemcache module
madvfree module
protobuf module
ristretto module
rueidis module
valkey-go module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL