Documentation
¶
Overview ¶
Package cachekit provides a small caching toolkit for services that use Redis or Valkey as a shared cache. It exposes a byte-oriented Cache interface, a pluggable codec layer, and a generic cache-aside helper (GetOrSet) with single-flight deduplication.
Index ¶
- Variables
- func GetOrSet[T any](ctx context.Context, c Cache, key string, ttl time.Duration, ...) (T, error)
- func GetOrSetMany[T any](ctx context.Context, c Cache, keys []string, ttl time.Duration, ...) (map[string]T, error)
- func GetOrSetManyWithCodec[T any](ctx context.Context, c Cache, keys []string, ttl time.Duration, codec Codec, ...) (map[string]T, error)
- func GetOrSetWithCodec[T any](ctx context.Context, c Cache, key string, ttl time.Duration, codec Codec, ...) (T, error)
- func IsMiss(err error) bool
- type BatchCache
- type Cache
- type Codec
- type Hooks
- type JSONCodec
- type MemoryCache
- func (c *MemoryCache) Codec() Codec
- func (c *MemoryCache) Delete(_ context.Context, key string) error
- func (c *MemoryCache) Get(_ context.Context, key string) ([]byte, error)
- func (c *MemoryCache) GetMany(_ context.Context, keys []string) (map[string][]byte, error)
- func (c *MemoryCache) Len() int
- func (c *MemoryCache) Set(_ context.Context, key string, val []byte, ttl time.Duration) error
- func (c *MemoryCache) SetMany(_ context.Context, items map[string][]byte, ttl time.Duration) error
- type Op
- type Option
- func WithAddr(addr string) Option
- func WithAuth(user, pass string) Option
- func WithClientSideCacheTTL(d time.Duration) Option
- func WithCodec(c Codec) Option
- func WithDB(db int) Option
- func WithDefaultTTL(d time.Duration) Option
- func WithEarlyRefresh(beta float64) Option
- func WithHooks(h Hooks) Option
- func WithNegativeTTL(d time.Duration) Option
- func WithOpTimeout(d time.Duration) Option
- func WithPrefix(prefix string) Option
- func WithStaleTTL(d time.Duration) Option
- func WithTTLJitter(fraction float64) Option
- type Options
- type RedisCache
- func (c *RedisCache) Close() error
- func (c *RedisCache) Codec() Codec
- func (c *RedisCache) Delete(ctx context.Context, key string) error
- func (c *RedisCache) Get(ctx context.Context, key string) ([]byte, error)
- func (c *RedisCache) GetMany(ctx context.Context, keys []string) (map[string][]byte, error)
- func (c *RedisCache) Ping(ctx context.Context) error
- func (c *RedisCache) Set(ctx context.Context, key string, val []byte, ttl time.Duration) error
- func (c *RedisCache) SetMany(ctx context.Context, items map[string][]byte, ttl time.Duration) error
- type ValkeyCache
- func (c *ValkeyCache) Close() error
- func (c *ValkeyCache) Codec() Codec
- func (c *ValkeyCache) Delete(ctx context.Context, key string) error
- func (c *ValkeyCache) Get(ctx context.Context, key string) ([]byte, error)
- func (c *ValkeyCache) GetMany(ctx context.Context, keys []string) (map[string][]byte, error)
- func (c *ValkeyCache) Ping(ctx context.Context) error
- func (c *ValkeyCache) Set(ctx context.Context, key string, val []byte, ttl time.Duration) error
- func (c *ValkeyCache) SetMany(ctx context.Context, items map[string][]byte, ttl time.Duration) error
Constants ¶
This section is empty.
Variables ¶
var ErrCacheMiss = errors.New("cachekit: cache miss")
ErrCacheMiss is returned by Cache.Get when the key does not exist. It is deliberately distinct from connection/infrastructure errors so callers can decide when to fall back to the source of truth.
var ErrNotFound = errors.New("cachekit: not found")
ErrNotFound signals "the entity does not exist at the source of truth" — distinct from ErrCacheMiss ("the cache has no entry"). Loaders return an error wrapping ErrNotFound to mean the lookup succeeded but found nothing; when the cache is configured with WithNegativeTTL, GetOrSet stores that fact so hot lookups of nonexistent entities stop hammering the database (cache penetration). Subsequent GetOrSet calls return ErrNotFound without invoking the loader until the negative entry expires.
Functions ¶
func GetOrSet ¶
func GetOrSet[T any](ctx context.Context, c Cache, key string, ttl time.Duration, loader func(context.Context) (T, error)) (T, error)
GetOrSet implements the cache-aside pattern: it returns the cached value for key, or invokes loader, stores the result, and returns it.
Concurrent callers that miss on the same key share a single loader call (see group.go). Waiters honor their own context: a canceled waiter returns ctx.Err() immediately without aborting the shared flight. The loader itself runs detached from the initiating caller's cancellation (values and any deadline are kept) so one canceled request cannot fail every waiter or waste a completed load.
Infrastructure errors from Get are treated as a miss so the service keeps working when the cache is down; in that degraded mode the loader result is still returned and Set is attempted best-effort. A cached entry that fails to decode (corrupted, or written by an incompatible schema version) is also treated as a miss: the entry is deleted and the loader repopulates it, instead of the key erroring until TTL expiry.
If the loader returns an error wrapping ErrNotFound and the cache is configured with WithNegativeTTL, the absence itself is cached: further calls return ErrNotFound without invoking the loader until the negative entry expires.
With WithStaleTTL, an entry past its freshness TTL is still served — immediately, with no loader latency — while a deduplicated background refresh replaces it (stale-while-revalidate). With WithEarlyRefresh, entries approaching their freshness deadline are probabilistically refreshed in the background before they expire, spreading reloads across time and across instances (XFetch). Both features store entries in a small metadata envelope; entries written without one (older deploys, raw Set calls) are treated as fresh until their backend TTL expires. Values are encoded with codec (nil means the package default, JSON).
func GetOrSetMany ¶
func GetOrSetMany[T any](ctx context.Context, c Cache, keys []string, ttl time.Duration, loader func(ctx context.Context, missing []string) (map[string]T, error)) (map[string]T, error)
GetOrSetMany is the batched cache-aside helper: it returns cached values for all keys it can, calls loader once with the keys that are missing, stores the loaded values, and returns the merged map.
The result contains an entry per key that exists; keys the loader omits are simply absent (and, with WithNegativeTTL, that absence is cached so they are not re-loaded until the negative entry expires). Omission is a positive statement — return an error, not an empty map, if absence could not be determined, or missing entities get negative-cached. A loader error fails the whole call. A broken cache degrades: read failures make every key load, write failures are best-effort, and undecodable entries self-heal exactly like GetOrSet.
Unlike GetOrSet, batch loads are not single-flighted: deduplicating overlapping batches would require per-key flights with partial joins. Duplicate keys in the input are deduplicated.
func GetOrSetManyWithCodec ¶
func GetOrSetManyWithCodec[T any](ctx context.Context, c Cache, keys []string, ttl time.Duration, codec Codec, loader func(ctx context.Context, missing []string) (map[string]T, error)) (map[string]T, error)
GetOrSetManyWithCodec is GetOrSetMany with an explicit Codec.
Types ¶
type BatchCache ¶
type BatchCache interface {
Cache
// GetMany returns the values stored at keys; missing keys are absent
// from the map. A non-nil error means the whole batch failed.
GetMany(ctx context.Context, keys []string) (map[string][]byte, error)
// SetMany stores all items with the same ttl (resolved per key:
// default fallback, jitter). Best-effort per key where the backend
// allows it; the returned error reflects the first failure.
SetMany(ctx context.Context, items map[string][]byte, ttl time.Duration) error
}
BatchCache is implemented by backends with efficient multi-key operations (all built-in backends implement it). GetOrSetMany uses it when available and falls back to per-key Get/Set otherwise.
type Cache ¶
type Cache interface {
// Get returns the raw value stored at key, or ErrCacheMiss.
Get(ctx context.Context, key string) ([]byte, error)
// Set stores val at key. ttl <= 0 means "use the backend's default
// TTL"; if the backend has no default, the key does not expire.
Set(ctx context.Context, key string, val []byte, ttl time.Duration) error
// Delete removes key. Deleting a missing key is not an error.
Delete(ctx context.Context, key string) error
}
Cache is the minimal contract every backend must satisfy. Implementations must return ErrCacheMiss (possibly wrapped) from Get when the key is absent, and reserve other errors for real failures.
type Codec ¶
Codec converts values to and from the raw bytes stored in the cache. Implementations must be safe for concurrent use.
type Hooks ¶
type Hooks struct {
// OnHit / OnMiss fire on every Get, keyed by the logical key.
OnHit func(key string)
OnMiss func(key string)
// OnError fires for every backend failure — including errors cachekit
// swallows by design, such as the best-effort Set after a load in
// GetOrSet. Wire this to a counter or a degraded cache stays invisible.
OnError func(op Op, key string, err error)
// OnLoad fires after every GetOrSet loader invocation with its
// duration and outcome. Loader calls are the cache's whole reason to
// exist — this is the number to alert on. For batch loads
// (GetOrSetMany) it fires once per missing key, all with the batch's
// duration and outcome.
OnLoad func(key string, dur time.Duration, err error)
}
Hooks receives cache events, for wiring metrics (hit ratio, error counters, loader latency) and logging. The zero value is valid and disables everything; individual callbacks may be nil.
Callbacks run synchronously on the request path and may be invoked concurrently — keep them fast and thread-safe (atomic counters, not blocking I/O).
type JSONCodec ¶
type JSONCodec struct{}
JSONCodec encodes values as JSON. It is the package default: readable, schema-free, and good enough until profiling says otherwise (swap in msgpack/protobuf via WithCodec or GetOrSetWithCodec when it does).
type MemoryCache ¶
type MemoryCache struct {
// contains filtered or unexported fields
}
MemoryCache is a process-local Cache backed by a mutex-guarded map. It is meant for tests, local development, and as an L1 in front of a shared backend — it does no eviction beyond TTL expiry, so do not use it for unbounded key sets.
Expired entries are dropped lazily on Get; there is no background reaper. Values are copied on Set and Get so callers can't alias the stored bytes.
func NewMemory ¶
func NewMemory(opts ...Option) *MemoryCache
NewMemory returns an empty in-memory cache. Addr/Auth/DB options are ignored; Prefix, DefaultTTL and Codec apply as usual.
func (*MemoryCache) Codec ¶
func (c *MemoryCache) Codec() Codec
Codec returns the codec configured for this cache (default JSON).
func (*MemoryCache) Delete ¶
func (c *MemoryCache) Delete(_ context.Context, key string) error
Delete removes key. Deleting a missing key succeeds.
func (*MemoryCache) GetMany ¶
GetMany returns the values stored at keys under a single lock acquisition. Missing keys are simply absent from the result map.
func (*MemoryCache) Len ¶
func (c *MemoryCache) Len() int
Len reports the number of stored entries, including not-yet-reaped expired ones. Intended for tests and metrics.
type Op ¶
type Op string
Op identifies which cache operation an OnError hook fires for.
const ( OpGet Op = "get" OpSet Op = "set" OpDelete Op = "delete" // OpDecode fires when a cached entry fails to decode (corrupted or // written by an incompatible schema version). GetOrSet self-heals by // deleting the entry and falling back to the loader. OpDecode Op = "decode" // OpEncode fires when a loaded value fails to encode. GetOrSetMany // still returns the value to the caller — it just isn't cached. OpEncode Op = "encode" // OpRefresh fires when a background refresh flight (stale-while- // revalidate or early refresh) fails for any reason — loader error, // encode failure, panic, or timeout. Nothing waits on these flights, // so this hook is the only place their failures are visible. OpRefresh Op = "refresh" )
type Option ¶
type Option func(*Options)
Option mutates Options in the functional-options style.
func WithClientSideCacheTTL ¶
func WithDefaultTTL ¶
func WithEarlyRefresh ¶
func WithNegativeTTL ¶
func WithOpTimeout ¶
func WithPrefix ¶
func WithStaleTTL ¶
func WithTTLJitter ¶
type Options ¶
type Options struct {
// Addr is the host:port of the Redis/Valkey server.
Addr string
// Username and Password authenticate the connection (ACL or requirepass).
Username string
Password string
// DB selects the logical database (ignored in cluster mode).
DB int
// Prefix is prepended to every key, typically "service-name:".
// It namespaces keys so services can share one server safely.
Prefix string
// DefaultTTL applies when Set is called with ttl <= 0.
// Zero means keys without an explicit TTL never expire.
DefaultTTL time.Duration
// Codec overrides the default JSON codec for GetOrSet helpers
// bound to this cache (see RedisCache.Codec).
Codec Codec
// ClientSideCacheTTL enables server-assisted client-side caching on
// backends that support it (currently ValkeyCache): reads are served
// from a local LRU for at most this duration, with the server pushing
// invalidations on writes. Zero disables it. Ignored by backends
// without client-side caching.
ClientSideCacheTTL time.Duration
// Hooks receives hit/miss/error/loader events for metrics and logging.
// See the Hooks type; the zero value disables everything.
Hooks Hooks
// TTLJitter spreads every stored TTL uniformly within ±(fraction ×
// TTL), so keys written together don't all expire together (cache
// avalanche). 0.1 means ±10%. Zero disables; values are capped at 0.9.
TTLJitter float64
// NegativeTTL enables negative caching in GetOrSet/GetOrSetMany: when
// the loader reports ErrNotFound (or, for batch loads, omits a key),
// that absence is cached for this duration and served as ErrNotFound
// without re-invoking the loader. Zero disables negative caching.
NegativeTTL time.Duration
// OpTimeout bounds each individual cache operation (Get/Set/Delete/
// batch/Ping) with its own deadline, so a slow cache can't hold
// requests hostage — a slow cache is worse than a down one. It does
// not bound GetOrSet loaders. Zero means no per-op deadline. Ignored
// by MemoryCache.
OpTimeout time.Duration
// StaleTTL enables stale-while-revalidate in GetOrSet: after an
// entry's freshness TTL passes, it stays servable for this long while
// a deduplicated background refresh reloads it — hot keys never pay
// loader latency on the request path. Entries are stored in a small
// metadata envelope (see envelope.go). GetOrSetMany serves stale
// entries but does not background-refresh them; they reload at hard
// expiry (freshness TTL + StaleTTL). Zero disables.
StaleTTL time.Duration
// EarlyRefreshBeta enables probabilistic early refresh (XFetch): as
// an entry approaches its freshness deadline, each GetOrSet hit
// refreshes it in the background with a probability that rises with
// proximity to expiry, scaled by the loader's observed duration.
// Because every process rolls independently, a fleet spreads its
// reloads instead of stampeding the database at expiry — this is the
// cross-instance complement to per-process single-flight. 1.0 is a
// sensible default; larger refreshes earlier. Zero disables.
EarlyRefreshBeta float64
}
Options configures a cache backend. Zero values are usable defaults: localhost:6379, no prefix, no default TTL, JSON codec.
type RedisCache ¶
type RedisCache struct {
// contains filtered or unexported fields
}
RedisCache implements Cache on top of go-redis v9. Because go-redis speaks plain RESP it works unchanged against both Redis and Valkey.
To switch to a different client (e.g. valkey-go for auto-pipelining and server-assisted client-side caching), add a sibling file implementing the same Cache interface — callers depend only on cachekit.Cache.
func New ¶
func New(opts ...Option) *RedisCache
New dials a Redis/Valkey server from Options and returns a cache. The returned cache owns the connection; call Close when done.
func NewWithClient ¶
func NewWithClient(client redis.UniversalClient, opts ...Option) *RedisCache
NewWithClient wraps an existing go-redis client (single node, cluster, or sentinel). The caller keeps ownership of the client's lifecycle; Close on the returned cache is a no-op. Addr/Auth/DB options are ignored — only Prefix, DefaultTTL and Codec apply.
func (*RedisCache) Close ¶
func (c *RedisCache) Close() error
Close releases the underlying client if this cache created it (New). For NewWithClient it is a no-op — the caller owns the client.
func (*RedisCache) Codec ¶
func (c *RedisCache) Codec() Codec
Codec returns the codec configured for this cache (default JSON), for use with GetOrSetWithCodec.
func (*RedisCache) Delete ¶
func (c *RedisCache) Delete(ctx context.Context, key string) error
Delete removes key. Deleting a missing key succeeds.
func (*RedisCache) GetMany ¶
GetMany returns the values stored at keys in one MGET round trip. Missing keys are simply absent from the result map.
func (*RedisCache) Ping ¶
func (c *RedisCache) Ping(ctx context.Context) error
Ping verifies connectivity, for health checks.
type ValkeyCache ¶
type ValkeyCache struct {
// contains filtered or unexported fields
}
ValkeyCache implements Cache on top of valkey-go, which adds auto-pipelining and optional server-assisted client-side caching on top of plain RESP. It works against both Valkey and Redis servers.
When a client-side cache TTL is configured (WithClientSideCacheTTL), reads go through DoCache: hot keys are served from the client's local LRU, the server invalidates them on writes from any client, and valkey-go deduplicates concurrent cache-filling GETs on its own. Note that cachekit's GetOrSet single-flight (group.go) still applies — it deduplicates *loader* (database) calls, which no client can do.
Degradation caveat: unlike go-redis (which gives up after a few retries), valkey-go retries network errors until the context is done. When the server is unreachable, Get/Set block for the full context deadline before GetOrSet can fall through to the loader — so always pass request-scoped contexts with deadlines, or inject a client built with DisableRetry via NewValkeyWithClient if you prefer fail-fast.
func NewValkey ¶
func NewValkey(opts ...Option) (*ValkeyCache, error)
NewValkey dials a Valkey/Redis server from Options and returns a cache. The returned cache owns the connection; call Close when done.
Server-assisted client-side caching (CLIENT TRACKING) is enabled only when WithClientSideCacheTTL is set; otherwise the client is created with tracking disabled so it also works against servers without RESP3 invalidation support.
func NewValkeyWithClient ¶
func NewValkeyWithClient(client valkey.Client, opts ...Option) *ValkeyCache
NewValkeyWithClient wraps an existing valkey-go client. The caller keeps ownership of the client's lifecycle; Close on the returned cache is a no-op. Addr/Auth/DB options are ignored. If WithClientSideCacheTTL is used with a client created with DisableCache, valkey-go silently falls back to plain reads — no error, just no client-side caching.
func (*ValkeyCache) Close ¶
func (c *ValkeyCache) Close() error
Close releases the underlying client if this cache created it (NewValkey). For NewValkeyWithClient it is a no-op.
func (*ValkeyCache) Codec ¶
func (c *ValkeyCache) Codec() Codec
Codec returns the codec configured for this cache (default JSON).
func (*ValkeyCache) Delete ¶
func (c *ValkeyCache) Delete(ctx context.Context, key string) error
Delete removes key. Deleting a missing key succeeds.
func (*ValkeyCache) Get ¶
Get returns the value at key, or ErrCacheMiss if absent. With a client-side cache TTL configured, the read is served from the local cache when possible.
func (*ValkeyCache) GetMany ¶
GetMany returns the values stored at keys in one auto-pipelined round trip (through the client-side cache when WithClientSideCacheTTL is set, like Get). Missing keys are simply absent from the result map.
func (*ValkeyCache) Ping ¶
func (c *ValkeyCache) Ping(ctx context.Context) error
Ping verifies connectivity, for health checks.