Documentation
¶
Overview ¶
Package cachex is a two-tier cache engine: a sharded in-process LRU (L1) in front of a shared store such as memcached (L2), with stampede protection, namespaces and stats.
Consistency: L1 is per process. After a Delete on one node, other nodes may serve their L1 copy for up to the L1 TTL (WithL1TTL). L2 is the shared source of truth.
Index ¶
- Variables
- type CASStore
- type Cache
- func (c *Cache) Close() error
- func (c *Cache) Delete(ctx context.Context, key string) (err error)
- func (c *Cache) Get(ctx context.Context, key string) ([]byte, error)
- func (c *Cache) GetMulti(ctx context.Context, keys []string) (map[string][]byte, error)
- func (c *Cache) GetOrLoad(ctx context.Context, key string, ttl time.Duration, load Loader) ([]byte, error)
- func (c *Cache) GetOrLoadMulti(ctx context.Context, keys []string, ttl time.Duration, load MultiLoader) (map[string][]byte, error)
- func (c *Cache) GetView(ctx context.Context, key string, fn func(val []byte) error) error
- func (c *Cache) Namespace(name string) (*Namespace, error)
- func (c *Cache) Set(ctx context.Context, key string, val []byte, ttl time.Duration) error
- func (c *Cache) SetMulti(ctx context.Context, items map[string][]byte, ttl time.Duration) (err error)
- func (c *Cache) Stats() Stats
- type Clock
- type Compressor
- type Events
- type Invalidation
- type Invalidator
- type Loader
- type MultiCASGetter
- type MultiGetter
- type MultiLoader
- type Namespace
- func (n *Namespace) Delete(ctx context.Context, key string) error
- func (n *Namespace) Get(ctx context.Context, key string) ([]byte, error)
- func (n *Namespace) GetMulti(ctx context.Context, keys []string) (map[string][]byte, error)
- func (n *Namespace) GetOrLoad(ctx context.Context, key string, ttl time.Duration, load Loader) ([]byte, error)
- func (n *Namespace) GetOrLoadMulti(ctx context.Context, keys []string, ttl time.Duration, load MultiLoader) (map[string][]byte, error)
- func (n *Namespace) GetView(ctx context.Context, key string, fn func(val []byte) error) error
- func (n *Namespace) Invalidate(ctx context.Context) error
- func (n *Namespace) Set(ctx context.Context, key string, val []byte, ttl time.Duration) error
- func (n *Namespace) SetMulti(ctx context.Context, items map[string][]byte, ttl time.Duration) error
- type Op
- type OpInfo
- type Option
- func WithBeta(b float64) Option
- func WithClock(cl Clock) Option
- func WithCompression(c Compressor, minSize int) Option
- func WithDecompressors(cs ...Compressor) Option
- func WithDefaultTTL(d time.Duration) Option
- func WithDegradedL1TTL(d time.Duration) Option
- func WithDeleteMarkerTTL(d time.Duration) Option
- func WithDistributedLock(ttl, poll time.Duration) Option
- func WithEvents(e Events) Option
- func WithInvalidator(inv Invalidator) Option
- func WithL1Frequency(on bool) Option
- func WithL1MaxBytes(n int64) Option
- func WithL1MaxEntries(n int) Option
- func WithL1TTL(d time.Duration) Option
- func WithL2(s Store) Option
- func WithLoadTimeout(d time.Duration) Option
- func WithNegativeTTL(d time.Duration) Option
- func WithPublishTimeout(d time.Duration) Option
- func WithShards(n int) Option
- func WithStaleIfError(d time.Duration) Option
- func WithStaleWindow(d time.Duration) Option
- func WithSweepInterval(d time.Duration) Option
- func WithTTLJitter(f float64) Option
- func WithVersionTTL(d time.Duration) Option
- type Stats
- type Store
- type Tier
Constants ¶
This section is empty.
Variables ¶
var ( ErrMiss = errors.New("cachex: miss") ErrNotStored = errors.New("cachex: not stored") ErrClosed = errors.New("cachex: closed") ErrInvalidKey = errors.New("cachex: invalid key") ErrValueTooLarge = errors.New("cachex: value too large") // ErrNotFound is returned by a Loader when the source has no such record. With // WithNegativeTTL the absence is cached and GetOrLoad returns ErrNotFound without loading. ErrNotFound = errors.New("cachex: not found") // ErrLoaderPanic wraps a panic recovered from a Loader. It is returned, never cached. ErrLoaderPanic = errors.New("cachex: loader panicked") )
Sentinel errors. A miss is ErrMiss, never (nil, nil).
Functions ¶
This section is empty.
Types ¶
type CASStore ¶
type CASStore interface {
// Gets returns the value and an opaque token for CompareAndSwap; ErrMiss when absent.
Gets(ctx context.Context, key string) (val []byte, token any, err error)
// CompareAndSwap stores val only if key is unchanged since Gets returned token.
// It returns ErrNotStored when the key changed and ErrMiss when it is gone.
CompareAndSwap(ctx context.Context, key string, val []byte, token any, ttl time.Duration) error
}
CASStore is an optional Store capability. With it, loads publish with compare-and-swap and Delete leaves a short-lived marker, so a load that read its source before a Delete on any node cannot write the old value back afterwards. Without it that guard is per process only.
type Cache ¶
type Cache struct {
// contains filtered or unexported fields
}
Cache is safe for concurrent use. Create with New, release with Close.
func (*Cache) Delete ¶
Delete removes key from both tiers. Unlike Set, an L2 failure is returned, because a value left in L2 would be served to every node.
func (*Cache) Get ¶
Get returns a copy of the cached value, or ErrMiss. L2 failures degrade to a miss.
func (*Cache) GetMulti ¶
GetMulti returns the cached values that are present; missing keys are simply absent. L1 answers first; the rest go to L2 in one round trip when the store implements MultiGetter.
func (*Cache) GetOrLoad ¶
func (c *Cache) GetOrLoad(ctx context.Context, key string, ttl time.Duration, load Loader) ([]byte, error)
GetOrLoad returns the cached value or computes it with load, once per key per process. Near expiry it refreshes early (XFetch); within the stale window it serves the old value and refreshes in the background. Loader errors are returned and not cached.
func (*Cache) GetOrLoadMulti ¶
func (c *Cache) GetOrLoadMulti(ctx context.Context, keys []string, ttl time.Duration, load MultiLoader) (map[string][]byte, error)
GetOrLoadMulti returns cached values for keys and loads the rest with one loader call. Unlike GetOrLoad there is no single-flight across concurrent batches, no stale window and no early refresh. Keys the source doesn't have are absent from the result.
func (*Cache) GetView ¶ added in v0.2.0
GetView calls fn with the cached value without copying it, or returns ErrMiss. The slice is valid only during fn and must not be modified or retained; copy it to keep it. A later Set of the same key doesn't change a slice already passed to fn. fn's error is returned. On an L1 hit GetView does not allocate.
func (*Cache) Namespace ¶
Namespace returns a handle. name must be non-empty, at most 64 bytes, printable ASCII without ':' or spaces, and must not start with "cachex".
func (*Cache) Set ¶
Set writes val to L2 then L1. ttl <= 0 uses the default TTL. A failed L2 write is not returned as an error (the cache is best effort); L1 then keeps the value only for the degraded TTL and Stats.L2Errors grows.
type Compressor ¶ added in v0.3.0
type Compressor interface {
ID() byte
// Compress appends the compressed form of src to dst and returns the extended slice.
Compress(dst, src []byte) ([]byte, error)
// Decompress appends the decompressed form of src to dst and returns the extended slice.
// It must return an error, not panic, on malformed input.
Decompress(dst, src []byte) ([]byte, error)
}
Compressor compresses L2 values. Implementations must be safe for concurrent use.
The ID is written into every compressed L2 entry and selects the decoder on read, so it must be stable across versions and unique among the codecs a deployment uses. 0 is reserved for "raw".
type Events ¶ added in v0.3.0
type Events struct {
// OpStart runs before a public operation; the returned context (e.g. carrying a span)
// is used for L2 calls and the loader. Returning nil keeps ctx.
OpStart func(ctx context.Context, op Op, key string) context.Context
// OpEnd runs after the operation with the context OpStart returned.
OpEnd func(ctx context.Context, info OpInfo, dur time.Duration)
// LoadEnd runs after each single-key loader call, including background refreshes.
LoadEnd func(ctx context.Context, key string, dur time.Duration, err error)
// L2Error reports a store call that counted as a breaker failure. op is the Store
// method, e.g. "get", "set", "add", "delete", "get_multi", "gets", "cas".
L2Error func(op string, err error)
// BreakerChange reports a breaker transition ("closed", "open", "half-open").
BreakerChange func(from, to string)
// LoaderPanic reports a recovered loader panic. key is empty for GetOrLoadMulti.
LoaderPanic func(key string, recovered any)
// PublishError reports a failed Invalidator.Publish.
PublishError func(msg Invalidation, err error)
}
Events are optional hooks for logging, metrics and tracing. Nil fields are skipped. Hooks run synchronously on the calling goroutine (BreakerChange and L2Error may run on a background refresh), so they must be fast and must not call back into the Cache.
func MergeEvents ¶ added in v0.3.0
MergeEvents combines hooks; each is called in argument order. OpStart contexts chain, so later hooks see earlier values. A hook no argument sets stays nil.
func SlogEvents ¶ added in v0.3.0
SlogEvents logs rare, actionable events to l: L2 errors and publish errors at Warn, breaker opening at Warn (other transitions at Info), loader panics at Error. Per-operation hooks stay nil, so the hot path is unaffected. Combine with other hooks via MergeEvents.
type Invalidation ¶
type Invalidation struct {
Keys []string // single keys removed with Delete
Namespace string // namespace bumped with Invalidate
}
Invalidation tells other processes to drop L1 copies.
type Invalidator ¶
type Invalidator interface {
Publish(ctx context.Context, msg Invalidation) error
// Subscribe registers fn and returns; the implementation delivers until ctx is done.
Subscribe(ctx context.Context, fn func(Invalidation)) error
}
Invalidator broadcasts invalidations between processes (Redis pub/sub, NATS, ...), making cross-node L1 staleness sub-second instead of bounded by the L1 TTL. Delivery is best effort; the TTL bounds still hold for lost messages.
type MultiCASGetter ¶ added in v0.3.0
type MultiCASGetter interface {
GetsMulti(ctx context.Context, keys []string) (vals map[string][]byte, tokens map[string]any, err error)
}
MultiCASGetter is an optional CASStore capability: Gets for many keys in one round trip, used by GetOrLoadMulti. Absent keys are missing from both maps.
type MultiGetter ¶
type MultiGetter interface {
GetMulti(ctx context.Context, keys []string) (map[string][]byte, error)
}
MultiGetter is an optional Store capability used by Cache.GetMulti for one round trip. The result holds only keys that were found.
type MultiLoader ¶
MultiLoader loads the given missing keys at once. Keys absent from the result don't exist; with WithNegativeTTL that absence is cached.
type Namespace ¶
type Namespace struct {
// contains filtered or unexported fields
}
Namespace groups keys so they can be invalidated together with one atomic L2 counter bump. Data keys are "<name>:<version>:<key>"; the version lives in L2 at "cachex:ns:<name>:v". Other processes see an invalidation within the version TTL (WithVersionTTL).
func (*Namespace) Get ¶
Get reads key in the namespace. An unknown version (L2 down, never seen) is a miss.
func (*Namespace) GetMulti ¶ added in v0.4.0
GetMulti reads keys in the namespace; absent keys are omitted. An unknown version (L2 down, never seen) reports every key as absent.
func (*Namespace) GetOrLoad ¶
func (n *Namespace) GetOrLoad(ctx context.Context, key string, ttl time.Duration, load Loader) ([]byte, error)
GetOrLoad is Cache.GetOrLoad inside the namespace. If the version is unknown the loader runs without caching, so callers still get a value.
func (*Namespace) GetOrLoadMulti ¶ added in v0.4.0
func (n *Namespace) GetOrLoadMulti(ctx context.Context, keys []string, ttl time.Duration, load MultiLoader) (map[string][]byte, error)
GetOrLoadMulti is Cache.GetOrLoadMulti inside the namespace; load always sees the caller's keys. If the version is unknown the loader runs without caching, so callers still get values.
func (*Namespace) GetView ¶ added in v0.4.0
GetView is Cache.GetView inside the namespace: fn sees the value without a copy and must not retain it. An unknown version (L2 down, never seen) is a miss.
func (*Namespace) Invalidate ¶
Invalidate makes every key in the namespace unreachable on all nodes by bumping the version. Old entries age out of both tiers; this process also frees its L1 copies at once.
type Op ¶ added in v0.3.0
type Op uint8
Op names a public Cache operation in Events.
type OpInfo ¶ added in v0.3.0
OpInfo describes a finished operation. Key is empty and Keys is the batch size for multi-key operations. For multi-key reads Tier is the farthest tier any key reached (L1 < L2 < Loaded); for writes it is TierMiss.
type Option ¶
type Option func(*config) error
Option configures a Cache.
func WithBeta ¶
WithBeta sets XFetch probabilistic early refresh strength (0 disables). Default 1.0.
func WithCompression ¶ added in v0.3.0
func WithCompression(c Compressor, minSize int) Option
WithCompression compresses values of at least minSize bytes before writing them to L2 (Set, SetMulti, GetOrLoad, GetOrLoadMulti). A value is stored raw when compressing fails or doesn't make it smaller. L1 always holds the uncompressed value, so L1 reads and GetView are unaffected. c is also registered as a decoder (see WithDecompressors).
Roll out in two deploys: first ship a version where every node can decode the codec (WithDecompressors, or WithCompression with a huge minSize), then enable compression. Nodes that can't decode an entry treat it as a miss and count Stats.DecodeErrors.
func WithDecompressors ¶ added in v0.3.0
func WithDecompressors(cs ...Compressor) Option
WithDecompressors lets the cache read L2 entries compressed with these codecs without compressing its own writes. Uncompressed entries are always readable.
func WithDefaultTTL ¶
WithDefaultTTL is used when Set/GetOrLoad receive ttl <= 0. Default 5m.
func WithDegradedL1TTL ¶
WithDegradedL1TTL is the L1 TTL used when a write to L2 fails. Default 1s.
func WithDeleteMarkerTTL ¶
WithDeleteMarkerTTL sets how long Delete leaves a marker in L2 when the store implements CASStore. A load that read its source before the Delete fails to publish while the marker lives, so the TTL must exceed the load timeout. Default 2 * load timeout.
func WithDistributedLock ¶
WithDistributedLock makes GetOrLoad take a lock in L2 before loading, so across all processes one loader runs per key. Others poll every poll until the value appears or ttl passes, then load themselves. ttl should exceed the slowest load. Needs WithL2.
func WithEvents ¶ added in v0.3.0
WithEvents installs hooks for logging, metrics and tracing. Combine several with MergeEvents.
func WithInvalidator ¶
func WithInvalidator(inv Invalidator) Option
WithInvalidator broadcasts Delete and Namespace.Invalidate to other processes.
func WithL1Frequency ¶ added in v0.2.0
WithL1Frequency makes L1 eviction frequency-aware (default true): a count-min sketch tracks how often keys are read and written, and eviction prefers rarely used entries over popular ones. On a Zipf workload it raised hit ratio ~2 points for ~15% slower parallel Get. A value just written is never evicted by its own Set either way.
func WithL1MaxBytes ¶
WithL1MaxBytes bounds the in-process tier by approximate bytes (0 = unlimited).
func WithL1MaxEntries ¶
WithL1MaxEntries bounds the in-process tier by entry count (0 = unlimited). Default 100000.
func WithL1TTL ¶
WithL1TTL caps how long a per-process copy lives. It is the cross-node staleness bound: L1 is eventually consistent with L2 within this window. Default 10s.
func WithLoadTimeout ¶
WithLoadTimeout bounds each loader call. Default 5s.
func WithNegativeTTL ¶
WithNegativeTTL caches a Loader's ErrNotFound for d, so repeated lookups of absent records don't hit the source. 0 (default) disables.
func WithPublishTimeout ¶ added in v0.5.1
WithPublishTimeout bounds how long Delete and Namespace.Invalidate wait for the invalidator to accept a broadcast (default: the load timeout). Broker clients such as franz-go retry until their context ends, so without a bound an unreachable broker would block every Delete. A publish that times out is reported through Events.PublishError; the local delete has already happened.
func WithStaleIfError ¶ added in v0.4.0
WithStaleIfError lets GetOrLoad answer with the last good value for d past its expiry when the loader fails, so a source outage degrades to stale data instead of errors. It covers transport and server failures, not ErrNotFound, which is an authoritative answer from the source. The stale answer reports TierStale and counts StaleOnError. Default 0 (off): loader errors reach the caller.
func WithStaleWindow ¶
WithStaleWindow lets GetOrLoad serve an expired value for d while it refreshes in the background.
func WithSweepInterval ¶
WithSweepInterval sets the expired-entry janitor period (0 disables; expiry stays lazy). Default 1s.
func WithTTLJitter ¶
WithTTLJitter shortens every stored TTL by a random fraction in [0, f), so keys written together don't expire together. f must be in [0, 1). 0 (default) disables.
func WithVersionTTL ¶
WithVersionTTL sets how long a namespace version is cached locally, which is the staleness window after Namespace.Invalidate on other processes. Default 2s.
type Stats ¶
type Stats struct {
L1Hits, L1Misses uint64
L2Hits, L2Misses uint64
L2Errors uint64 // transport failures
L2Skipped uint64 // calls short-circuited by the open breaker
Loads, LoadErrors uint64
StaleServed uint64
StaleOnError uint64 // stale answers served because the loader failed
EarlyRefreshes uint64
DecodeErrors uint64
NegativeHits uint64 // cached ErrNotFound answers
LockWaits uint64 // loads that waited on another process's lock
PublishErrors uint64 // failed invalidation broadcasts
LoadsDiscarded uint64 // loads not cached because Set/Delete raced them
Evictions, Expirations uint64
Entries int
Bytes int64
Breaker string
}
Stats is a point-in-time snapshot. Counters are read independently, so a snapshot taken under load is not atomic across fields.
type Store ¶
type Store interface {
// Get returns ErrMiss when the key is absent.
Get(ctx context.Context, key string) ([]byte, error)
// Set stores val. ttl <= 0 means no expiry.
Set(ctx context.Context, key string, val []byte, ttl time.Duration) error
// Add stores only if absent, else ErrNotStored.
Add(ctx context.Context, key string, val []byte, ttl time.Duration) error
// Delete of a missing key is success.
Delete(ctx context.Context, key string) error
// Incr adds delta to a decimal counter; ErrMiss when absent.
Incr(ctx context.Context, key string, delta uint64) (uint64, error)
Close() error
}
Store is the shared L2 tier (memcached or Redis in production, an in-memory fake in tests). Values are opaque encoded envelopes. Implementations must be safe for concurrent use.
type Tier ¶ added in v0.3.0
type Tier uint8
Tier reports who answered an operation.
const ( TierL1 Tier = iota // in-process copy TierL2 // shared store, backfilled into L1 TierStale // expired value served within the stale window TierMiss // nothing cached; also used by writes and failed calls TierLoaded // loader ran (here or in a shared flight) TierNegative // cached ErrNotFound )
Tiers reported in OpInfo.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Command example shows L1-only cachex usage: GetOrLoad, a Namespace, Invalidate and Stats.
|
Command example shows L1-only cachex usage: GetOrLoad, a Namespace, Invalidate and Stats. |
|
internal
|
|
|
breaker
Package breaker is a small circuit breaker guarding the L2 cache tier so a dead L2 costs zero latency: while Open, Allow returns false immediately.
|
Package breaker is a small circuit breaker guarding the L2 cache tier so a dead L2 costs zero latency: while Open, Allow returns false immediately. |
|
clock
Package clock provides a cheap, coarse wall-clock reading for hot paths.
|
Package clock provides a cheap, coarse wall-clock reading for hot paths. |
|
counter
Package counter provides a striped uint64 counter for hot paths where many goroutines increment the same statistic.
|
Package counter provides a striped uint64 counter for hot paths where many goroutines increment the same statistic. |
|
envelope
Package envelope is the binary wrapper stored in L2.
|
Package envelope is the binary wrapper stored in L2. |
|
fastrand
Package fastrand exposes the runtime's cheap per-thread random source for hot paths that only need to spread work over stripes, not statistical quality.
|
Package fastrand exposes the runtime's cheap per-thread random source for hot paths that only need to spread work over stripes, not statistical quality. |
|
flight
Package flight provides context-aware singleflight for cache stampede protection.
|
Package flight provides context-aware singleflight for cache stampede protection. |
|
lru
Package lru implements a sharded, byte-valued LRU cache with per-entry TTL.
|
Package lru implements a sharded, byte-valued LRU cache with per-entry TTL. |
|
sketch
Package sketch implements a lock-free count-min sketch with 4-bit saturating counters and periodic aging, used to estimate recent access frequency (TinyLFU).
|
Package sketch implements a lock-free count-min sketch with 4-bit saturating counters and periodic aging, used to estimate recent access frequency (TinyLFU). |
|
kafka
module
|
|
|
Package memcached implements cachex.Store over github.com/bradfitz/gomemcache.
|
Package memcached implements cachex.Store over github.com/bradfitz/gomemcache. |
|
Package memstore is an in-memory cachex.Store fake with fault injection for tests.
|
Package memstore is an in-memory cachex.Store fake with fault injection for tests. |
|
nats
module
|
|
|
prometheus
module
|
|
|
rabbitmq
module
|
|
|
redis
module
|
|
|
Package storetest is a conformance suite for cachex.Store implementations.
|
Package storetest is a conformance suite for cachex.Store implementations. |
|
Package typed wraps a cachex backend with a Codec so callers work with values, not bytes.
|
Package typed wraps a cachex backend with a Codec so callers work with values, not bytes. |