Documentation
¶
Overview ¶
Package smartcache is a small, type-safe caching library.
It provides a generic read-through / write-through cache (Cache[T]) over a pluggable byte key-value backend (the CacheStore interface). Cache[T] owns serialization (JSON by default), enforces a positive TTL backstop so a missed or failed eviction can only cause bounded staleness, applies downward-only TTL jitter by default so keys written together don't expire in sync, optionally negative-caches "not found" results, and de-duplicates concurrent cold loads with singleflight.
Every Cache[T] is built through a Manager: NewManager creates one over a CacheStore and a set of defaults, and the package-level generic function Register creates a named, type-safe Cache[T] on it (Go methods cannot have type parameters, so registration cannot be a method on Manager). GetMany batches a read-through lookup for several keys in one round trip, calling the caller's batch loader at most once for whatever keys are not already cached.
The library performs no logging itself: Get, GetMany, and Put return an Outcome so the caller can meter cache-hit rate and detect populate failures without any of the library's own log lines. A Manager can optionally be configured with WithOTLP to export per-cache metrics (hit/miss/load/evict counters and a load-latency histogram) over OpenTelemetry OTLP, fire-and- forget on a background interval.
The backend is injected as an interface, so Redis (see subpackage redisstore) can be swapped for any other store (see subpackage memstore) without touching call sites.
Index ¶
- Variables
- type AliasCacheStore
- type AliasRef
- type AliasWriteSpec
- type BatchCacheStore
- type Cache
- func (c *Cache[T]) Evict(ctx context.Context, key string) error
- func (c *Cache[T]) EvictByAlias(ctx context.Context, alias AliasRef) error
- func (c *Cache[T]) EvictMany(ctx context.Context, keys ...string) error
- func (c *Cache[T]) Get(ctx context.Context, key string, loader Loader[T]) (*T, Outcome, error)
- func (c *Cache[T]) GetByAlias(ctx context.Context, alias AliasRef, loader Loader[T]) (*T, Outcome, error)
- func (c *Cache[T]) GetMany(ctx context.Context, keys []string, ...) (map[string]*T, error)
- func (c *Cache[T]) Put(ctx context.Context, key string, writer Writer[T]) (*T, Outcome, error)
- func (c *Cache[T]) PutAliased(ctx context.Context, primaryKey string, alias AliasRef, writer Writer[T]) (*T, Outcome, error)
- func (c *Cache[T]) PutAliasedValue(ctx context.Context, primaryKey string, alias AliasRef, val *T) error
- func (c *Cache[T]) PutValue(ctx context.Context, key string, val *T) error
- type CacheStore
- type Codec
- type EntityOptions
- type Loader
- type Manager
- type ManagerOption
- func WithDefaultCodec(c Codec) ManagerOption
- func WithDefaultDisableSingleflight(b bool) ManagerOption
- func WithDefaultJitterFraction(f float64) ManagerOption
- func WithDefaultNegativeTTL(d time.Duration) ManagerOption
- func WithDefaultTTL(d time.Duration) ManagerOption
- func WithOTLP(cfg OTLPConfig) ManagerOption
- type OTLPConfig
- type Options
- type Outcome
- type PrimaryKeyed
- type Writer
Constants ¶
This section is empty.
Variables ¶
var ( // ErrStoreMiss is returned by CacheStore.Get when the key is absent. It is an // internal miss signal that Cache[T] handles; it is never surfaced to callers. ErrStoreMiss = errors.New("smartcache: store miss") // ErrNotFound is the sentinel a Loader returns (or wraps) to signal a cacheable // "does not exist". When negative caching is enabled Cache[T] remembers it // briefly and returns it from Get. ErrNotFound = errors.New("smartcache: not found") // ErrInvalidTTL is returned by Register when the resolved TTL <= 0 and // AllowInfinite is false. ErrInvalidTTL = errors.New("smartcache: TTL must be > 0 (set AllowInfinite for no expiry)") // ErrPointerType is the value Register panics with when T is itself a pointer // type. Cache[T] already returns *T from every method; T being a pointer // too makes that a double pointer (e.g. **User), which opens a hole where // a non-nil outer pointer can wrap a nil inner one — silently violating // the "a successful Get/Put never returns nil" guarantee the rest of this // package relies on. This is a programming error at the call site // (Register[*User] instead of Register[User]), not a runtime condition, so // Register panics with it instead of returning it. ErrPointerType = errors.New("smartcache: T must not itself be a pointer type") // ErrNilWrite is returned by Put when writer succeeds (nil error) but // returns a nil value. The cache is never set to nil, so this is treated // as a contract violation, not a cacheable state. ErrNilWrite = errors.New("smartcache: writer returned a nil value with no error") // ErrNilStore is returned by NewManager when the CacheStore is nil. ErrNilStore = errors.New("smartcache: store must not be nil") // ErrEmptyName is returned by Register when the cache name is empty. The name // is required: it is both the metric name and the default key prefix. ErrEmptyName = errors.New("smartcache: cache name must not be empty") // ErrDuplicateName is returned by Register when a cache name is already // registered on the manager. Each registered cache must have a unique name. ErrDuplicateName = errors.New("smartcache: cache name already registered") // ErrEmptyPrefix is returned by Register when EntityOptions.Prefix is explicitly // set to an empty string. Prefix namespaces every key this cache stores; an empty // prefix would collide with any other cache that also opts out of namespacing. ErrEmptyPrefix = errors.New("smartcache: prefix must not be empty") // ErrInvalidJitterFraction is returned by Register when the resolved jitter // fraction is outside [0, 1). Zero disables jitter. ErrInvalidJitterFraction = errors.New("smartcache: jitter fraction must be in [0, 1)") // ErrNotAliasGroup is returned when an alias-only method (GetByAlias, PutAliased, // PutAliasedValue, EvictByAlias) is called on a cache that was not created with // RegisterAliasGroup. ErrNotAliasGroup = errors.New("smartcache: cache is not an alias group") // ErrAliasingNotSupported is used in the RegisterAliasGroup panic when the manager's // store does not implement AliasCacheStore. ErrAliasingNotSupported = errors.New("smartcache: store does not implement AliasCacheStore") )
Functions ¶
This section is empty.
Types ¶
type AliasCacheStore ¶
type AliasCacheStore interface {
CacheStore
// GetByAlias resolves pointerKey -> value key -> value bytes. It returns ErrStoreMiss when
// the pointer or the value it points to is absent.
GetByAlias(ctx context.Context, pointerKey string) ([]byte, error)
// PutByAlias writes spec.Value at spec.ValueKey and, when spec.PointerKey is non-empty,
// upserts the alias pointer (one-alias-per-field replacement plus cross-primary steal
// cleanup) and adds it to the members set — then refreshes every current group key to
// spec.TTL. It is one atomic operation on the {ns} slot.
PutByAlias(ctx context.Context, spec *AliasWriteSpec) error
// EvictByPrimary deletes the value key, every pointer listed in the members set, and the
// members set itself.
EvictByPrimary(ctx context.Context, valueKey, membersKey string) error
// EvictByAlias resolves pointerKey to its primary, then cascades exactly like EvictByPrimary.
// valueKeyPrefix and membersKeyPrefix let the store derive the primary and members key.
EvictByAlias(ctx context.Context, pointerKey, valueKeyPrefix, membersKeyPrefix string) error
}
AliasCacheStore is an optional CacheStore extension for backends that can maintain atomic key groups (a value key, its alias pointers, and a members set) in one operation. It is detected once at RegisterAliasGroup via a comma-ok type assertion, mirroring BatchCacheStore. The store is a dumb executor: Cache[T] builds every key string via keyspace.go and passes them in, so the keyspace stays single-source and the store never constructs keys.
type AliasRef ¶
AliasRef names one secondary lookup key for an alias-group cache: a field ("email") and a value ("foo@bar.com").
type AliasWriteSpec ¶
type AliasWriteSpec struct {
ValueKey string // bc:{ns}:<primary>
MembersKey string // bc:memb:{ns}:<primary>
PointerKey string // bc:grp:{ns}:<field>:<value> ("" => primary-only write)
FieldPrefix string // bc:grp:{ns}:<field>: ("" => primary-only write)
ValueKeyPrefix string // bc:{ns}: (steal-cleanup: parse old primary from old value key)
MembersKeyPrefix string // bc:memb:{ns}: (steal-cleanup: rebuild old primary's members key)
Value []byte
TTL time.Duration // the single jittered TTL, computed once by Cache[T]
}
AliasWriteSpec carries the pre-built key strings (produced by keyspace.go on the Cache[T] side) for a single grouped write. The alias-related fields are empty for a primary-only value write, which still refreshes the TTL of every existing group key.
type BatchCacheStore ¶
type BatchCacheStore interface {
CacheStore
// GetMany returns the raw bytes for the keys that are present. Keys that are
// absent (or expired) are omitted from the returned map — a miss is never an
// error here.
GetMany(ctx context.Context, keys []string) (map[string][]byte, error)
}
BatchCacheStore is an optional extension of CacheStore for backends that can read many keys in one round trip. Cache[T].GetMany uses it when the injected store implements it (redisstore does, via MGET); stores that do not are handled transparently by GetMany's per-key fallback.
type Cache ¶
type Cache[T any] struct { // contains filtered or unexported fields }
Cache is a generic, type-safe read-through / delete-on-write cache over a CacheStore. It is constructed only via Register on a Manager, never directly.
func Register ¶
Register creates a Cache[T] on m under name (required, unique; it doubles as the metric name and the default key prefix). It panics with ErrPointerType if T is a pointer type. It returns ErrEmptyName, ErrEmptyPrefix, ErrDuplicateName, ErrInvalidTTL, or ErrInvalidJitterFraction on invalid input. A failed Register never consumes the name.
func RegisterAliasGroup ¶
RegisterAliasGroup registers an alias-group cache: one cached value reachable by several alias keys, with all bookkeeping maintained atomically by the store. It behaves like Register but additionally (a) requires the manager's store to implement AliasCacheStore and (b) requires T to implement PrimaryKeyed (so GetByAlias read-through can learn a value's primary key). Like Register's pointer-type check, it panics on a misconfiguration that must fail at init: a pointer T, a store without AliasCacheStore, or a T that is not PrimaryKeyed.
func (*Cache[T]) Evict ¶
Evict deletes key (delete-on-write). It returns the delete error so the caller can retry or alarm; the TTL backstop bounds staleness if it fails.
func (*Cache[T]) EvictByAlias ¶
EvictByAlias deletes the whole group reachable through alias (value + every pointer + members set). Only valid on an alias-group cache.
func (*Cache[T]) Get ¶
Get reads through to loader on a cache miss. See Outcome for the result modes.
Sharing note: when singleflight is enabled (the default), a Loaded or LoadedNotCached result may be the exact same *T handed to every concurrent caller deduped onto the same loader call — that is singleflight.Do's own contract. Treat a Loaded/LoadedNotCached result as read-only; copy it before mutating. A Hit result is always freshly unmarshaled per call and is never shared with another caller.
func (*Cache[T]) GetByAlias ¶
func (c *Cache[T]) GetByAlias(ctx context.Context, alias AliasRef, loader Loader[T]) (*T, Outcome, error)
GetByAlias reads a value by one of its alias keys. It is read-through: on a miss it runs loader, learns the loaded value's primary key via PrimaryKeyed, and rebuilds the group (value + this alias pointer + members) under one TTL. Only valid on an alias-group cache.
func (*Cache[T]) GetMany ¶
func (c *Cache[T]) GetMany( ctx context.Context, keys []string, loadMissing func(ctx context.Context, missing []string) (map[string]*T, error), ) (map[string]*T, error)
GetMany reads several keys in one batch: cache hits (and warm negative hits) are served without touching loadMissing; keys not found in the cache are collected and loaded in ONE loadMissing call, then populated back into the cache (with per-key downward jitter, on both the positive TTL and NegativeTTL). Keys loadMissing does not return are negative-cached (when NegativeTTL > 0) and omitted from the result map. Unlike Get, GetMany is never deduplicated via singleflight.
func (*Cache[T]) Put ¶
Put performs a write-through: it calls writer to persist the value to your source of truth, then caches exactly the value writer returned. See Outcome for the result modes.
If writer fails, its error is returned unchanged and the cache is untouched. If writer succeeds but returns a nil value, Put returns ErrNilWrite and the cache is untouched. If the cache-side write fails after writer succeeded, Put still returns the value with Outcome == WrittenNotCached — the real write already happened; only caching it failed, and that must never look like a failed write to the caller.
writer is never deduplicated the way Get's loader is: two concurrent Put calls for the same key are two distinct writes, and singleflight would silently drop one of them.
func (*Cache[T]) PutAliased ¶
func (c *Cache[T]) PutAliased(ctx context.Context, primaryKey string, alias AliasRef, writer Writer[T]) (*T, Outcome, error)
PutAliased write-throughs writer's value under primaryKey and registers alias as one of its lookup keys (one-alias-per-field: re-registering a field replaces its old value). Only valid on an alias-group cache.
type CacheStore ¶
type CacheStore interface {
// Get returns the raw bytes for key, or ErrStoreMiss if the key is absent.
Get(ctx context.Context, key string) ([]byte, error)
// Set stores val under key with the given ttl. A ttl <= 0 means no expiry.
Set(ctx context.Context, key string, val []byte, ttl time.Duration) error
// Delete removes key. Deleting an absent key is not an error.
Delete(ctx context.Context, key string) error
// Exists reports whether key is present (and not expired).
Exists(ctx context.Context, key string) (bool, error)
}
CacheStore is the backend abstraction Cache[T] depends on: a byte key-value cache store — never the application's own database. Cache[T] owns all (de)serialization, so CacheStore never sees the cached type T. Swapping the backend (Redis, in-memory, anything) means providing a different CacheStore implementation; the Cache[T] API is unchanged.
type Codec ¶
Codec serializes cached values to and from bytes. The default (when Options.Codec is nil) is encoding/json.
type EntityOptions ¶
type EntityOptions struct {
Prefix *string
TTL *time.Duration
AllowInfinite *bool
JitterFraction *float64
NegativeTTL *time.Duration
DisableSingleflight *bool
Codec Codec
}
EntityOptions overrides manager defaults for one registered cache. Every field is an optional pointer: nil inherits the manager default, non-nil overrides it.
type Loader ¶
Loader loads a value from the source of truth on a cache miss.
Contract: return (val, nil) on success, (nil, ErrNotFound) for a cacheable not-found, or (nil, err) for a transient error (which is never cached).
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager owns the shared backend store, global defaults, and (optionally) the OTLP meter provider. Caches are created against it with Register.
func NewManager ¶
func NewManager(store CacheStore, opts ...ManagerOption) (*Manager, error)
NewManager builds a Manager over store. It returns ErrNilStore if store is nil. When WithOTLP was supplied (and no meter was injected for tests), it stands up the OTLP exporter + periodic reader + meter provider.
type ManagerOption ¶
ManagerOption configures a Manager in NewManager.
func WithDefaultCodec ¶
func WithDefaultCodec(c Codec) ManagerOption
WithDefaultCodec sets the default codec (caches fall back to JSON when nil).
func WithDefaultDisableSingleflight ¶
func WithDefaultDisableSingleflight(b bool) ManagerOption
WithDefaultDisableSingleflight sets the default singleflight toggle.
func WithDefaultJitterFraction ¶
func WithDefaultJitterFraction(f float64) ManagerOption
WithDefaultJitterFraction sets the default downward-jitter fraction (0 disables).
func WithDefaultNegativeTTL ¶
func WithDefaultNegativeTTL(d time.Duration) ManagerOption
WithDefaultNegativeTTL sets the default negative-cache TTL (0 disables negative caching).
func WithDefaultTTL ¶
func WithDefaultTTL(d time.Duration) ManagerOption
WithDefaultTTL sets the default positive TTL inherited by caches that do not override EntityOptions.TTL.
func WithOTLP ¶
func WithOTLP(cfg OTLPConfig) ManagerOption
WithOTLP enables OpenTelemetry OTLP metric export. It returns an error if the config's URL is nil.
type OTLPConfig ¶
type OTLPConfig struct {
// URL is the OTLP gRPC endpoint as host:port (e.g. "localhost:4317"). Required.
URL *string
// FlushInterval is how often the background PeriodicReader exports. Default 15s.
FlushInterval *time.Duration
// Timeout bounds a single export. Default 10s.
Timeout *time.Duration
// Insecure uses plaintext gRPC when true. Default false.
Insecure *bool
// ServiceName becomes the resource service.name attribute. Default "smartcache".
ServiceName *string
}
OTLPConfig configures the optional OpenTelemetry OTLP metric exporter on a Manager (via WithOTLP). Every field is an optional pointer; a nil field falls back to its documented default. URL is required — WithOTLP returns an error if it is nil.
type Options ¶
type Options struct {
// Prefix namespaces keys: the stored key is bc:<Prefix>:<key> for a normal
// cache and bc:{<Prefix>}:<key> for an alias-group cache (built in keyspace.go).
Prefix string
// TTL is the positive backstop expiry applied to cached values. Required unless
// AllowInfinite is true.
TTL time.Duration
// AllowInfinite opts in to TTL <= 0 (entries never expire).
AllowInfinite bool
// NegativeTTL, when > 0, enables negative caching of ErrNotFound for that
// duration. Zero disables negative caching.
NegativeTTL time.Duration
// Codec overrides serialization. Nil means JSON.
Codec Codec
// DisableSingleflight turns off de-duplication of concurrent cold loads.
DisableSingleflight bool
}
Options is the resolved configuration for a Cache[T], produced by Register.
type Outcome ¶
type Outcome int
Outcome describes how Cache[T].Get or Cache[T].Put served a request, so the caller can meter hit rate and alarm on populate failures without the library logging anything. Hit, Loaded, LoadedNotCached, and NegativeHit are Get-only (they name a load that happened on a read). Written and WrittenNotCached are Put-only (they name a write that happened, not a load) — Put never returns a Get-only value and Get never returns a Put-only value.
const ( // Hit means Get served the value from cache. Hit Outcome = iota // Loaded means Get missed, the loader ran, and the value was cached. Loaded // LoadedNotCached means Get missed, the loader ran, but writing the value // back to the store failed. The value is still returned; the read never fails. LoadedNotCached // NegativeHit means Get served a cached "not found" marker. NegativeHit // Written means Put's writer succeeded and the value was cached. Written // WrittenNotCached means Put's writer succeeded, but writing the value to // the store failed. The value is still returned; the write never fails on // account of the cache. WrittenNotCached )
type PrimaryKeyed ¶
type PrimaryKeyed interface {
CachePrimaryKey() string
}
PrimaryKeyed is implemented by the value type T (or *T) cached in an alias-group cache. It lets the library learn a value's primary key when rebuilding the group on a GetByAlias read-through miss. It returns the primary key VALUE (e.g. "5"); the value key is bc:{ns}:<value>.
type Writer ¶
Writer persists a value to the source of truth and returns exactly what was written, so Put can cache that same value.
Contract: return (val, nil) with val != nil on success. Any non-nil error is returned to the caller unchanged and nothing is cached. Returning (nil, nil) violates the contract — Put returns ErrNilWrite, because the cache is never set to a nil value.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package memstore provides an in-memory smartcache.CacheStore for unit tests and light use.
|
Package memstore provides an in-memory smartcache.CacheStore for unit tests and light use. |
|
Package redisstore provides a go-redis-backed smartcache.CacheStore.
|
Package redisstore provides a go-redis-backed smartcache.CacheStore. |