cache

package module
v4.2.0 Latest Latest
Warning

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

Go to latest
Published: Oct 29, 2026 License: MIT Imports: 16 Imported by: 0

README

cache

CI CodeQL Coverage Mutation Documentation Go Reference Release Go License

cache is a typed Go cache library with explicit hit, miss, stale, decode, and backend-failure semantics. It provides bounded cache-aside loading, versioned and hashed keys, strict codecs, a bounded memory backend, and native Redis and Valkey adapters. Valkey-backed caches can also publish a refresh only while a compatible distributed lease is still owned.

The semantic API does not expose Redis or Valkey client types. Backends keep their native clients and atomic behavior while applications share one portable contract.

V4 is the current major. Upgrading from an earlier major follows the migration guide, including v3's adoption of go-redis v9.22's expanded custom-client interface. For ecosystem-wide selection and ownership guidance, see the versioned Golib ecosystem index and its Persistence and durability family.

Install

go get github.com/faustbrian/go-cache/v4@v4.2.0

Go 1.27.0 or newer is required.

Quickstart

package main

import (
	"context"
	"fmt"
	"time"

	cache "github.com/faustbrian/go-cache/v4"
	cachememory "github.com/faustbrian/go-cache/v4/adapters/memory"
)

type User struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

func main() {
	ctx := context.Background()
	backend, err := cachememory.New(cachememory.Config{
		MaxEntries: 10_000,
		MaxBytes:   64 << 20,
		Clock:      cache.SystemClock{},
	})
	if err != nil {
		panic(err)
	}

	keys, err := cache.NewKeySpace("accounts", "user", 1,
		cache.StringKeyEncoder{}, 128)
	if err != nil {
		panic(err)
	}
	users, err := cache.New(cache.Config[string, User]{
		Backend:  backend,
		Keys:     keys,
		Codec:    cache.JSONCodec[User]{Version: 1},
		TTL:      cache.TTLPolicy{TTL: 5 * time.Minute},
		Clock:    cache.SystemClock{},
		MaxValue: 1 << 20,
		Load: cache.LoadPolicy{
			MaxConcurrent:    64,
			MaxWaitersPerKey: 256,
			NegativeTTL:      30 * time.Second,
		},
	})
	if err != nil {
		panic(err)
	}
	defer users.Close()

	result, err := users.GetOrLoad(ctx, "user-42",
		func(ctx context.Context, id string) (cache.LoadResult[User], error) {
			// Replace this with a context-aware source lookup.
			return cache.LoadResult[User]{Value: User{ID: id, Name: "Ada"}, Found: true}, nil
		})
	if err != nil {
		panic(err)
	}
	fmt.Println(result.State, result.Value.Name)
}

Adopting v4

V4 is the current major; v3.0.0 and v2.0.0 remain published. The migration guide covers each major boundary, including the v4 removal of the deprecated backend/*, cacheservice and observability/* paths.

Always inspect Result.State; a stored zero value can be a Hit. A miss is not an error. Backend, decoding, schema, policy, limit, and loader failures remain errors and can be classified with errors.Is.

For distributed refreshes, acquire a Valkey lease through github.com/faustbrian/go-lease/valkey, derive its opaque guard with Store.Guard, and pass that guard to Cache.SetIfOwned or Cache.SetNegativeIfOwned. The cache record and active owner/token comparison occur in one Valkey script. Protected negative publication uses the configured NegativeTTL. The lease store and cache backend must use the same standalone Valkey deployment.

Choose a policy

  • Use plain cache-aside for data where a source lookup is affordable.
  • Add a short NegativeTTL only when source-level absence is authoritative.
  • Return LoadResult.Expiry or call SetWithExpiry when the source fixes an entry's deadline; the zero Expiry keeps the configured TTL.
  • Use StaleWhileRevalidate when low latency is more important than returning the refresh error.
  • Use StaleIfError when callers must see refresh failures but may also use the stale value. It returns both the stale Result and the error.
  • Do not enable both stale policies; construction rejects ambiguous precedence.

See policy decisions and failure modes before enabling stale behavior.

Backends and observability

Use the root package for portable cache policy and result semantics. Select a package under adapters/ only for its named integration, cachetest only for backend conformance tests, and an observability adapter only when the corresponding integration is required. Do not use this module as durable authoritative storage, a distributed-lock implementation, or an application-wide retry owner.

Service lifecycle

adapters/service.New adapts an explicit concrete cache, Redis, or Valkey resource to service.Component. Startup validation and readiness are opt-in callbacks that receive the service context and the concrete resource. Callers add the readiness check to service.Plan only when cache availability is required to accept new work.

Omitting Shutdown keeps the resource shared and guarantees the adapter never closes it. Providing Shutdown explicitly transfers close ownership. The adapter performs no retries, closes a transferred resource once after draining later-declared components, and preserves startup and partial-cleanup failures.

Version 4 removes the former backend/*, cacheservice, and observability/* import paths. Use the corresponding adapters/* package; configuration and runtime semantics are unchanged.

Documentation

Use the documentation index for behavior, backends, operations, adoption, compatibility, and security guidance.

Development and verification guidance is in CONTRIBUTING.md. Start with untagged go test ./... for deterministic tests and fuzz seeds. The bounded Make gates retain manifest-selected Redis/Valkey services and integration tags as well as focused loader/shutdown race assertions; they are not a Docker-free substitute for direct Go tests. Mutation is optional local or scheduled assurance, not a PR prerequisite.

License

MIT. See LICENSE.

Documentation navigation

Complete documentation map · Current source package map

Use the versioned ecosystem entry for package selection. The frozen compatibility cohort is separate from current package releases and unpublished source changes. Adopting them does not silently update that cohort.

Documentation

Overview

Package cache provides typed, backend-independent cache semantics.

Cache distinguishes fresh Hit, absent/negative Miss, Stale and errors. Backend records own retained payload bytes; generic K and V are not deep copied. A published found load shares the loader's original V with its waiters, so mutable values require application ownership or an explicitly copying codec/loader.

Construct with New and a validated NewKeySpace. Dependencies are retained, must be concurrency-safe, and must bound their own work. Only loader and observer notification panics are isolated; other callbacks are trusted. Observers run synchronously, and some publication callbacks run under a per-flight mutation lock. Do not re-enter that protected boundary.

Caller cancellation detaches one load waiter. Load work uses a separate cache-owned context; Shutdown cancels and joins it within the caller's bound, without closing a backend/client or joining unrelated foreground calls. Incomplete shutdown is not proof that admitted external side effects stopped.

Index

Examples

Constants

View Source
const MaxLoadFlights = 1 << 16

MaxLoadFlights bounds configured loader concurrency and retained flight work.

Variables

View Source
var (
	// ErrMiss identifies an explicit cache miss where an error value is needed.
	ErrMiss = errors.New("cache miss")
	// ErrBackend identifies a storage or transport failure.
	ErrBackend = errors.New("cache backend error")
	// ErrDecode identifies malformed serialized data.
	ErrDecode = errors.New("cache decode error")
	// ErrSchemaMismatch identifies an incompatible payload version.
	ErrSchemaMismatch = errors.New("cache schema mismatch")
	// ErrInvalidKey identifies invalid key configuration or encoding.
	ErrInvalidKey = errors.New("invalid cache key")
	// ErrKeyTooLarge identifies a backend key beyond its configured bound.
	ErrKeyTooLarge = errors.New("cache key too large")
	// ErrValueTooLarge identifies a payload beyond its configured bound.
	ErrValueTooLarge = errors.New("cache value too large")
	// ErrInvalidTTL identifies an invalid or already expired deadline.
	ErrInvalidTTL = errors.New("invalid cache TTL")
	// ErrCapacity identifies a record that cannot fit a bounded backend.
	ErrCapacity = errors.New("cache capacity exceeded")
	// ErrClosed identifies use after cache or backend shutdown.
	ErrClosed = errors.New("cache backend closed")
	// ErrShutdownIncomplete identifies shutdown ending with active loader work.
	ErrShutdownIncomplete = errors.New("cache shutdown incomplete")
	// ErrLoader identifies a source loader failure.
	ErrLoader = errors.New("cache loader error")
	// ErrLoaderPanic identifies a recovered loader panic.
	ErrLoaderPanic = errors.New("cache loader panic")
	// ErrLoadTimeout identifies a loader failure after LoadPolicy.Timeout expired.
	ErrLoadTimeout = errors.New("cache load timeout")
	// ErrRecursiveLoad identifies a loader re-entering the same cache.
	ErrRecursiveLoad = errors.New("recursive cache load")
	// ErrWaiterLimit identifies excess callers for one active key flight.
	ErrWaiterLimit = errors.New("cache waiter limit exceeded")
	// ErrFlightLimit identifies excess distinct-key load or refresh work.
	ErrFlightLimit = errors.New("cache flight limit exceeded")
	// ErrInvalidPolicy identifies invalid or contradictory policy options.
	ErrInvalidPolicy = errors.New("invalid cache policy")
	// ErrBatchTooLarge identifies a bulk request beyond its configured bound.
	ErrBatchTooLarge = errors.New("cache batch too large")
	// ErrInvalidRecord identifies malformed portable backend state.
	ErrInvalidRecord = errors.New("invalid cache record")
	// ErrInvalidConfig identifies invalid constructor dependencies or limits.
	ErrInvalidConfig = errors.New("invalid cache configuration")
	// ErrOwnershipLost identifies a protected write rejected by its backend.
	ErrOwnershipLost = errors.New("cache ownership lost")
	// ErrOwnershipUnsupported identifies a backend without atomic ownership validation.
	ErrOwnershipUnsupported = errors.New("cache ownership validation unsupported")
)

Functions

This section is empty.

Types

type Backend

type Backend interface {
	// Get returns a payload copy and explicit presence; failures are not misses.
	Get(context.Context, string) (Record, bool, error)
	// Set atomically applies condition and owns stored payload bytes on success.
	Set(context.Context, string, Record, Condition) (bool, error)
	// Delete atomically removes a key and reports presence, not value contents.
	Delete(context.Context, string) (bool, error)
}

Backend is the concurrent atomic storage contract implemented by adapters. Presence denotes provider retention, not freshness; Cache.Get separately enforces StaleAt using its clock. Server expiry may differ from that clock. Implementations must honor contexts and copy retained/returned payload bytes. Native clients, retries and shutdown remain provider/caller responsibilities; memory lazy expiration and server TTL precision are not identical.

type BulkResult

type BulkResult[K, V any] struct {
	// Key is the input key at the corresponding position.
	Key K
	// Result is the embedded lookup outcome; inspect Err before using it.
	Result[V]
	// Err is this key's failure; the bulk-level error only reports admission failure.
	Err error
}

BulkResult reports one GetMany result without flattening per-key errors.

type Cache

type Cache[K, V any] struct {
	// contains filtered or unexported fields
}

Cache provides concurrent typed cache operations over a retained backend. Use New; a zero/nil Cache is not usable. Dependencies must support concurrent calls, non-nil contexts are required except that Shutdown rejects nil, and callers own mutation of generic keys/values and dependency cleanup.

func New

func New[K, V any](config Config[K, V]) (*Cache[K, V], error)

New validates dependencies, TTL and load bounds and constructs an inert cache. Keys must already come from NewKeySpace; its zero value is not validated here. No goroutine is started until a flight is admitted. Config collaborators are retained by identity; construction does not clone or open backend resources.

func (*Cache[K, V]) Add

func (c *Cache[K, V]) Add(ctx context.Context, logical K, value V) (bool, error)

Add writes a value only when the key has no live record.

func (*Cache[K, V]) Close

func (c *Cache[K, V]) Close() error

Close cancels active loads, rejects new loads, and waits up to five seconds for loader/notification cleanup. It does not close the backend or client.

func (*Cache[K, V]) Delete

func (c *Cache[K, V]) Delete(ctx context.Context, logical K) (err error)

Delete removes a logical key. Deleting an absent key succeeds.

func (*Cache[K, V]) DeleteMany

func (c *Cache[K, V]) DeleteMany(ctx context.Context, keys []K) ([]MutationResult[K], error)

DeleteMany deletes keys sequentially in input order with per-key failures. There is no transaction or early stop after a failed/canceled item.

func (*Cache[K, V]) Get

func (c *Cache[K, V]) Get(ctx context.Context, logical K) (result Result[V], err error)

Get returns an explicit hit, negative/ordinary miss, or stale result. Freshness is Before(ExpiresAt); StaleAt is exclusive hard expiry. Hard expiry cleanup failures remain errors. Positive Decode ownership comes from Codec. Sliding fresh reads rewrite the record using IfPresent, not record-version compare-and-set, and can race a newer write. Stale/negative reads do not slide.

func (*Cache[K, V]) GetMany

func (c *Cache[K, V]) GetMany(ctx context.Context, keys []K) ([]BulkResult[K, V], error)

GetMany reads keys sequentially in input order without a pipeline/transaction. Admission failure is the top-level error; per-key errors do not stop the loop, even after cancellation. Generic keys/results are not deep copied.

func (*Cache[K, V]) GetOrLoad

func (c *Cache[K, V]) GetOrLoad(ctx context.Context, logical K, loader Loader[K, V]) (Result[V], error)

GetOrLoad returns a cached value or coalesces source work by encoded key. The first admitted flight's loader/key arguments serve every follower. Caller cancellation only detaches that waiter, including the last waiter. All followers of a published found load share its original V, not deep copies. Double checks or superseding mutations can instead return current decoded state. Stale-while-revalidate returns admission errors with stale data; later refresh errors are observed. Stale-if-error returns the original stale data plus error. Recursive calls with the supplied loader context reject the same Cache even for a different key; discarding that context bypasses detection. With InheritValues the admitting caller's values reach the loader; ctx's cancellation and deadline never do.

Example
package main

import (
	"context"
	"fmt"
	"time"

	cache "github.com/faustbrian/go-cache/v4"
	cachememory "github.com/faustbrian/go-cache/v4/adapters/memory"
)

func main() {
	backend, _ := cachememory.New(cachememory.Config{
		MaxEntries: 100,
		MaxBytes:   1 << 20,
		Clock:      cache.SystemClock{},
	})
	keys, _ := cache.NewKeySpace("example", "greeting", 1, cache.StringKeyEncoder{}, 128)
	store, _ := cache.New(cache.Config[string, string]{
		Backend:  backend,
		Keys:     keys,
		Codec:    cache.JSONCodec[string]{Version: 1},
		TTL:      cache.TTLPolicy{TTL: time.Minute},
		Clock:    cache.SystemClock{},
		MaxValue: 1024,
	})
	defer func() { _ = store.Close() }()

	result, err := store.GetOrLoad(context.Background(), "hello",
		func(context.Context, string) (cache.LoadResult[string], error) {
			return cache.LoadResult[string]{Value: "world", Found: true}, nil
		})
	fmt.Println(result.State == cache.Hit, result.Value, err)
}
Output:
true world <nil>

func (*Cache[K, V]) Replace

func (c *Cache[K, V]) Replace(ctx context.Context, logical K, value V) (bool, error)

Replace writes a value only when the key has a live record.

func (*Cache[K, V]) Set

func (c *Cache[K, V]) Set(ctx context.Context, logical K, value V) error

Set writes a value without an existence precondition.

func (*Cache[K, V]) SetIfOwned

func (c *Cache[K, V]) SetIfOwned(
	ctx context.Context,
	logical K,
	value V,
	guard OwnershipGuard,
) error

SetIfOwned atomically writes a value only while guard identifies the active backend owner. Ownership loss is reported as ErrOwnershipLost.

func (*Cache[K, V]) SetMany

func (c *Cache[K, V]) SetMany(ctx context.Context, entries []Entry[K, V]) ([]MutationResult[K], error)

SetMany writes entries sequentially in input order and records per-key errors. There is no rollback, transaction or early stop after a failed/canceled item.

func (*Cache[K, V]) SetNegativeIfOwned

func (c *Cache[K, V]) SetNegativeIfOwned(
	ctx context.Context,
	logical K,
	guard OwnershipGuard,
) error

SetNegativeIfOwned atomically writes an explicit negative record only while guard identifies the active backend owner. The configured NegativeTTL must be positive. Ownership loss is reported as ErrOwnershipLost.

func (*Cache[K, V]) SetWithExpiry added in v4.1.0

func (c *Cache[K, V]) SetWithExpiry(ctx context.Context, logical K, value V, expiry Expiry) error

SetWithExpiry writes a value without an existence precondition using a per-entry expiry. A zero Expiry behaves exactly like Set. An absolute deadline replaces the policy TTL while the configured StaleFor still follows it; NeverExpire ignores both. A deadline at or before the cache clock deletes the key instead of storing an expired value and still supersedes an active load. Non-zero expiries are rejected with ErrInvalidPolicy when the TTL policy is Sliding, because a sliding hit would replace the explicit deadline.

func (*Cache[K, V]) Shutdown

func (c *Cache[K, V]) Shutdown(ctx context.Context) error

Shutdown cancels active loads, rejects new loads, and waits for loader cleanup within ctx. A nil error means all active loads have completed, including synchronous load observers, not all independent foreground calls. Supplied backends/clients remain caller-owned and are not closed. ErrShutdownIncomplete permits late completion of already-admitted backend work when trusted callbacks or backends ignore cancellation.

type Clock

type Clock interface {
	// Now returns the time used for wall-clock deadlines; calls must be bounded.
	Now() time.Time
}

Clock supplies time for wall-clock expiration behavior. Implementations must be concurrency-safe, bounded and panic-free. Memory invokes Now under its mutex, so clock callbacks must not re-enter that backend.

type Codec

type Codec[V any] interface {
	// Encode serializes a borrowed V; callers must not mutate it during encoding.
	Encode(V) ([]byte, error)
	// Decode reads borrowed bytes and returns V with codec-defined ownership.
	Decode([]byte) (V, error)
}

Codec serializes and deserializes typed cache values. It is a retained concurrency-safe collaborator, not a generic deep-copy or panic boundary. Decode receives the retrieved record payload; resulting V ownership is defined by the codec. Cache does not clone its returned value.

type Condition

type Condition uint8

Condition controls the atomic precondition applied by Backend.Set.

const (
	// Unconditional always writes the record.
	Unconditional Condition = iota
	// IfAbsent writes only when the key does not hold a live record.
	IfAbsent
	// IfPresent writes only when the key holds a live record.
	IfPresent
)

type Config

type Config[K, V any] struct {
	// Backend is a retained collaborator; Cache shutdown does not close it.
	Backend Backend
	// Keys must be built by NewKeySpace; New does not validate a zero key space.
	Keys KeySpace[K]
	// Codec is retained and may be called concurrently; callbacks are not panic-isolated.
	Codec Codec[V]
	// TTL sets positive-record freshness, hard staleness and optional sliding reads.
	TTL TTLPolicy
	// Clock is retained; its Now method must be safe for concurrent bounded calls.
	Clock Clock
	// MaxValue is the positive encoded payload limit, excluding a backend wire header.
	MaxValue int
	// MaxBatch bounds sequential bulk items; zero selects 1000.
	MaxBatch int
	// Load configures process-local flight, loader, stale and negative policy.
	Load LoadPolicy
	// Jitter optionally supplies concurrent TTL sampling; nil selects RandomJitter when needed.
	Jitter JitterSource
	// Observer optionally receives synchronous events; Cache owns no exporter lifecycle.
	Observer Observer
}

Config contains all dependencies, limits, and policies for a Cache.

type Entry

type Entry[K, V any] struct {
	// Key is the logical key, borrowed without generic deep copying.
	Key K
	// Value is borrowed for serialization during its sequential Set.
	Value V
}

Entry pairs a logical key with a value for SetMany.

type Error

type Error struct {
	// Kind selects a stable category; unknown values add no sentinel.
	Kind ErrorKind
	// Operation is included in diagnostics; callers must supply a safe action name.
	Operation Operation
	// contains filtered or unexported fields
}

Error combines a stable semantic kind with a protected cause identity. Construct errors with NewError when a source cause must remain recognizable through errors.Is without exposing its text or concrete type.

func NewError

func NewError(kind ErrorKind, operation Operation, cause error) *Error

NewError creates a classified error whose cause remains recognizable with errors.Is but is hidden from formatting and errors.As.

func (*Error) Error

func (e *Error) Error() string

Error formats the action and category without formatting the protected cause.

func (*Error) Unwrap

func (e *Error) Unwrap() []error

Unwrap exposes category and protected identity for errors.Is, not the raw cause. It returns nil for a nil receiver or an uncategorized error without a cause.

type ErrorKind

type ErrorKind uint8

ErrorKind classifies an operation failure independently of its cause.

const (
	// BackendError identifies storage or transport failures.
	BackendError ErrorKind = iota + 1
	// DecodeError identifies malformed encoded values.
	DecodeError
	// SchemaMismatchError identifies incompatible payload versions.
	SchemaMismatchError
	// InvalidKeyError identifies invalid key configuration or encoding.
	InvalidKeyError
	// LimitError identifies a configured resource-limit violation.
	LimitError
	// PolicyError identifies an invalid or contradictory policy.
	PolicyError
	// LoaderError identifies a source loader failure.
	LoaderError
)

type Event

type Event struct {
	// Operation names the action; direct caller strings are not automatically redacted.
	Operation Operation
	// Outcome names the result; bundled cache producers use the defined constants.
	Outcome Outcome
	// Duration is non-negative cache-clock elapsed time for bundled producers.
	Duration time.Duration
	// Size is payload bytes for reads/writes, or retained key+payload bytes for removals.
	Size int
}

Event is a redacted semantic observation with no key or value fields.

type Expiry added in v4.1.0

type Expiry struct {
	// contains filtered or unexported fields
}

Expiry selects the freshness deadline of one entry. The zero value defers to the cache's TTLPolicy, RefreshJitter and NegativeTTL, so existing callers keep their fixed-TTL behavior. A non-zero Expiry replaces the fresh TTL and jitter for that entry: ExpireAt is absolute and NeverExpire never becomes stale. Expiry is an immutable value and is safe to share between goroutines.

func ExpireAt added in v4.1.0

func ExpireAt(deadline time.Time) Expiry

ExpireAt returns an absolute freshness deadline. A positive entry remains fresh before deadline and stays usable as Stale for the configured StaleFor after it; a negative entry ends at deadline. A deadline at or before the cache clock behaves like an already expired write: the key is deleted and nothing is stored. A zero or non-portable deadline, or one whose stale window passes the latest portable instant, is rejected with ErrInvalidTTL.

func NeverExpire added in v4.1.0

func NeverExpire() Expiry

NeverExpire returns an expiry that keeps an entry fresh until it is explicitly mutated or evicted by its backend. StaleFor does not apply. Remote backends store it with the longest server TTL the portable deadline allows, roughly two centuries from now, rather than a persistent key.

func (Expiry) IsZero added in v4.1.0

func (e Expiry) IsZero() bool

IsZero reports whether e defers to the cache's configured TTL policy.

type JSONCodec

type JSONCodec[V any] struct {
	// Version is the schema byte; Encode rejects zero, Decode only compares it.
	Version byte
	// MaxEncodedSize bounds bytes including the version; non-positive selects 1 MiB.
	MaxEncodedSize int
}

JSONCodec stores encoding/json data behind a one-byte schema version. Decode disallows unknown struct fields and trailing documents; map/interface fields and custom JSON methods retain encoding/json semantics. Version zero is rejected by Encode, not independently by Decode. The size cap is a wire bound, not a total heap/CPU bound or limit on user marshaling callbacks.

func (JSONCodec[V]) Decode

func (c JSONCodec[V]) Decode(encoded []byte) (V, error)

Decode validates size and schema version before strict JSON decoding.

func (JSONCodec[V]) Encode

func (c JSONCodec[V]) Encode(value V) ([]byte, error)

Encode serializes a value with its nonzero configured schema version. json.Marshal and custom marshaling work occur before the encoded-size check.

type JitterSource

type JitterSource interface {
	// Duration chooses TTL subtraction; custom results must be within the requested bound.
	Duration(time.Duration) time.Duration
}

JitterSource chooses how much time to subtract from a loaded value's TTL.

type KeyEncoder

type KeyEncoder[K any] interface {
	// EncodeKey returns deterministic borrowed key bytes or an error; size/work are caller-bounded.
	EncodeKey(K) ([]byte, error)
}

KeyEncoder deterministically converts a typed logical key to bytes.

type KeySpace

type KeySpace[K any] struct {
	// contains filtered or unexported fields
}

KeySpace hashes logical keys beneath a namespace, name, and version prefix.

func NewKeySpace

func NewKeySpace[K any](
	namespace string,
	name string,
	version uint32,
	encoder KeyEncoder[K],
	maxKeySize int,
) (KeySpace[K], error)

NewKeySpace validates and constructs an isolated versioned key space.

func (KeySpace[K]) Key

func (s KeySpace[K]) Key(logical K) (string, error)

Key hashes encoder bytes beneath the visible prefix. maxKeySize bounds the final backend key, not logical input bytes or encoder work. No panic recovery is provided. Use NewKeySpace; zero-value spaces have no encoder.

type LoadPolicy

type LoadPolicy struct {
	// MaxConcurrent bounds running loaders; zero selects 64.
	MaxConcurrent int
	// MaxFlights bounds distinct-key flights, including queued loads and
	// completed flights retained by an explicit mutation. Zero defaults to 1024.
	MaxFlights int
	// MaxWaitersPerKey bounds attached foreground callers per flight; zero selects 1024.
	MaxWaitersPerKey int
	// NegativeTTL is absence retention; zero disables storage of negative load
	// results unless the loader returns a non-zero LoadResult.Expiry.
	NegativeTTL time.Duration
	// StaleWhileRevalidate serves stale data and admits a shared background refresh.
	StaleWhileRevalidate bool
	// StaleIfError returns the originally read stale value with a foreground refresh error.
	StaleIfError bool
	// RefreshJitter is the maximum loaded-value TTL subtraction; it must be below TTL.
	RefreshJitter time.Duration
	// Timeout bounds each loader call from the moment it acquires a loader
	// slot; zero adds no bound and negative values are rejected. A loader
	// error after expiry matches ErrLoader and ErrLoadTimeout. Loaders that
	// ignore cancellation are not abandoned, and a late success is published.
	Timeout time.Duration
	// InheritValues exposes the context values, never the cancellation or
	// deadline, of the caller that admitted the flight to its loader. Joiners'
	// values are not visible. A stale-while-revalidate refresh inherits from
	// the stale reader that started it. The admitting context is retained
	// until the flight finishes. Use inherited values only for diagnostics
	// such as correlation and tracing: the result is shared by every waiter
	// and stored for later readers, so anything that changes it, such as a
	// tenant or principal, belongs in the key.
	InheritValues bool
}

LoadPolicy bounds loading and enables optional negative and stale behavior.

type LoadResult

type LoadResult[V any] struct {
	// Value is the loader result, shared without deep copying among waiters.
	Value V
	// Found distinguishes a present zero/nil value from authoritative absence.
	Found bool
	// Expiry optionally replaces the fresh TTL and jitter (found) or NegativeTTL
	// (absent) for this entry; zero keeps the cache policy. A non-zero Expiry on
	// an absent result is stored even when NegativeTTL is zero. A passed
	// deadline deletes the key; waiters still receive this result.
	Expiry Expiry
}

LoadResult is the value and existence result returned by a Loader.

type Loader

type Loader[K, V any] func(context.Context, K) (LoadResult[V], error)

Loader fetches a logical key from its source of truth. It receives a cache-owned context, never a waiter's cancellation or deadline; values are visible only with LoadPolicy.InheritValues. Honor its cancellation, which includes LoadPolicy.Timeout, and bound external calls; returned mutable V is shared among flight waiters. Errors are protected; panic values are discarded as ErrLoaderPanic.

type MutationResult

type MutationResult[K any] struct {
	// Key is the input key at the corresponding position.
	Key K
	// Err is this mutation's failure; other items are still attempted.
	Err error
}

MutationResult reports one bulk mutation error in input order.

type Observer

type Observer interface {
	// Observe receives an event synchronously; returned errors and panics are ignored by cache notification.
	Observe(context.Context, Event) error
}

Observer receives synchronous semantic events from cache operations. Notification discards returned errors and recovers observer panics, but cannot force a blocked callback to return. Implementations must bound work, support concurrent calls and avoid re-entering publication locks. Contexts can be caller-owned or cache-load-owned; event fields alone are not a redaction engine.

type Operation

type Operation string

Operation names the semantic cache action associated with an event or error.

const (
	// OperationGet identifies a read.
	OperationGet Operation = "get"
	// OperationSet identifies a write.
	OperationSet Operation = "set"
	// OperationDelete identifies invalidation.
	OperationDelete Operation = "delete"
	// OperationLoad identifies a source load.
	OperationLoad Operation = "load"
	// OperationEvict identifies capacity eviction.
	OperationEvict Operation = "evict"
	// OperationExpire identifies deadline expiration.
	OperationExpire Operation = "expire"
)

type Outcome

type Outcome string

Outcome is a low-cardinality semantic operation result.

const (
	// OutcomeSuccess identifies a successful mutation or load.
	OutcomeSuccess Outcome = "success"
	// OutcomeHit identifies a fresh read.
	OutcomeHit Outcome = "hit"
	// OutcomeMiss identifies an absent read.
	OutcomeMiss Outcome = "miss"
	// OutcomeStale identifies a stale read.
	OutcomeStale Outcome = "stale"
	// OutcomeNegative identifies a negative-cache result.
	OutcomeNegative Outcome = "negative"
	// OutcomeRejected identifies a failed conditional mutation.
	OutcomeRejected Outcome = "rejected"
	// OutcomeError identifies an operation failure.
	OutcomeError Outcome = "error"
	// OutcomeEvicted identifies capacity eviction.
	OutcomeEvicted Outcome = "evicted"
	// OutcomeExpired identifies deadline expiration.
	OutcomeExpired Outcome = "expired"
)

type OwnershipBackend

type OwnershipBackend interface {
	Backend
	// SetIfOwned compares ownership and publishes atomically, or reports loss/failure.
	SetIfOwned(context.Context, string, Record, OwnershipGuard) error
}

OwnershipBackend atomically validates ownership and writes one record.

type OwnershipGuard

type OwnershipGuard interface {
	// StorageKey returns the external lease storage coordinate; it may be sensitive.
	StorageKey() string
	// Owner returns the opaque identity to compare with the active owner.
	Owner() string
	// Token returns the opaque fencing identity to compare, not a numeric order.
	Token() string
}

OwnershipGuard identifies backend-authenticated ownership for one protected write. Methods must supply stable values during a call and must not panic. Implementations must return opaque storage coordinates, not logical keys or credentials.

type RandomJitter

type RandomJitter struct{}

RandomJitter samples a non-cryptographic duration in [0, max).

func (RandomJitter) Duration

func (RandomJitter) Duration(upperBound time.Duration) time.Duration

Duration returns a non-negative duration below max, or zero for max <= 0.

type Record

type Record struct {
	// Payload is caller-owned mutable encoded data; Clone isolates its bytes.
	Payload []byte
	// ExpiresAt is the first instant the value is no longer fresh.
	ExpiresAt time.Time
	// StaleAt is the hard usability deadline, at or after ExpiresAt.
	StaleAt time.Time
	// Negative marks authoritative absence and requires an empty payload.
	Negative bool
}

Record is the portable value and expiration envelope stored by a Backend.

func (Record) Clone

func (r Record) Clone() Record

Clone returns a portable record whose payload does not alias the receiver.

func (Record) Validate

func (r Record) Validate() error

Validate checks deadline portability and negative-record invariants. It does not compare deadlines with a clock or require a future hard expiry. Portable time must round-trip through signed nanoseconds since the Unix epoch.

type Result

type Result[V any] struct {
	// State is Hit, Miss or Stale on success; inspect the error before zero state.
	State State
	// Value is usable for Hit/Stale; generic references are not deep copied.
	Value V
	// Negative marks a cached or just-loaded authoritative-absence miss.
	Negative bool
}

Result is the explicit outcome of a typed cache read.

type State

type State uint8

State describes whether a cache lookup is fresh, absent, or stale.

const (
	// Hit indicates a fresh cached value.
	Hit State = iota + 1
	// Miss indicates no usable cached value.
	Miss
	// Stale indicates a decoded value beyond its fresh TTL but within StaleFor.
	Stale
)

type StringKeyEncoder

type StringKeyEncoder struct{}

StringKeyEncoder encodes strings without conversion.

func (StringKeyEncoder) EncodeKey

func (StringKeyEncoder) EncodeKey(key string) ([]byte, error)

EncodeKey returns an owned copy of key bytes without UTF-8 validation.

type SystemClock

type SystemClock struct{}

SystemClock uses the system wall clock.

func (SystemClock) Now

func (SystemClock) Now() time.Time

Now returns the current system time.

type TTLPolicy

type TTLPolicy struct {
	// TTL is the strictly positive fresh duration.
	TTL time.Duration
	// StaleFor is additional non-negative hard-retention time after freshness.
	StaleFor time.Duration
	// Sliding rewrites fresh records on hit with IfPresent, not record-version CAS.
	Sliding bool
}

TTLPolicy defines the fresh and stale lifetime of positive records.

func (TTLPolicy) Validate

func (p TTLPolicy) Validate() error

Validate reports whether the TTL and stale window are well formed.

Directories

Path Synopsis
adapters
memory
Package cachememory provides a bounded, concurrency-safe LRU cache adapter.
Package cachememory provides a bounded, concurrency-safe LRU cache adapter.
otel
Package cacheotel exports cache events as low-cardinality OpenTelemetry metrics.
Package cacheotel exports cache events as low-cardinality OpenTelemetry metrics.
redis
Package cacheredis adapts go-redis/v9 clients to the cache backend contract.
Package cacheredis adapts go-redis/v9 clients to the cache backend contract.
service
Package cacheservice adapts explicit cache resources to the service lifecycle without hiding their concrete types.
Package cacheservice adapts explicit cache resources to the service lifecycle without hiding their concrete types.
slog
Package cacheslog records redacted cache events with the standard log package.
Package cacheslog records redacted cache events with the standard log package.
valkey
Package cachevalkey adapts valkey-go clients to the cache backend contract.
Package cachevalkey adapts valkey-go clients to the cache backend contract.
Package cachetest provides a shared backend conformance suite.
Package cachetest provides a shared backend conformance suite.
internal
wire
Package wire encodes portable backend records for remote storage.
Package wire encodes portable backend records for remote storage.

Jump to

Keyboard shortcuts

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