Documentation
¶
Overview ¶
Package cache provides a small Cache port for data that is derived, re-computable, or externally sourced — an external API response, a normalized/aggregated read that is expensive to recompute but never the sole source of truth for it. It is explicitly NOT for domain reads (the database is the source of truth for those), sessions (a session store already owns that), or anything a domain-specific cache already governs directly. Losing the cache directory — corruption, a wiped disk, a fresh deployment — must always be a non-event: every consumer recomputes or re-fetches on a miss, never fails because the cache is empty or unavailable.
Two implementations: MemoryCache (in-process, for hermetic tests and as a caller's own boot-time fallback when the persistent backend fails to open) and BadgerCache (on-disk, persists across restarts — see that type's own doc for why it is the ONLY package outside this one allowed to import github.com/dgraph-io/badger/v4, enforced by a depguard rule in .golangci.yml).
Index ¶
- Variables
- type BadgerCache
- func (c *BadgerCache) Close() error
- func (c *BadgerCache) Delete(_ context.Context, key string) error
- func (c *BadgerCache) Get(_ context.Context, key string) ([]byte, bool, error)
- func (c *BadgerCache) RunGC(ctx context.Context, interval time.Duration)
- func (c *BadgerCache) RunValueLogGCOnce(discardRatio float64) error
- func (c *BadgerCache) Set(_ context.Context, key string, value []byte, ttl time.Duration) error
- type Cache
- type MemoryCache
Constants ¶
This section is empty.
Variables ¶
var ErrNegativeTTL = errors.New("cache: ttl must not be negative")
ErrNegativeTTL is returned by Set when ttl is negative. Only a ZERO ttl means "no expiration" (see Set's own doc); a negative ttl is always a caller bug — most likely a miscalculated duration — so both implementations reject it outright rather than silently treating it as "cache forever."
Functions ¶
This section is empty.
Types ¶
type BadgerCache ¶
type BadgerCache struct {
// contains filtered or unexported fields
}
BadgerCache is the persistent, on-disk Cache implementation, backed by github.com/dgraph-io/badger/v4. It is the ONLY file in this codebase allowed to import that package — every other consumer depends on the Cache port instead, enforced by a depguard rule in .golangci.yml scoped to this package. This keeps the dependency, and the corruption-recovery/GC operational concerns that come with an embedded LSM-tree store, fully contained here: nothing outside this file needs to know badger exists.
func NewBadgerCache ¶
func NewBadgerCache(dir string, logger *slog.Logger) (*BadgerCache, error)
NewBadgerCache opens (or creates) a BadgerCache at dir.
Corruption recovery: badger.Open failing is treated as a POTENTIALLY RECOVERABLE condition (e.g. an unclean shutdown after a Pi power loss, corrupting the value log or manifest), not a hard failure. It is logged loudly (Error level, with dir) and the ENTIRE directory is removed and Open retried exactly once. Losing the cache directory is, by this package's own design (see the package doc), always a non-event — every consumer recomputes or re-fetches on a miss — so discarding a corrupt cache is safe, and strictly better than either crashing boot over a cache or leaving a corrupt store the process can never open. If the retry ALSO fails, NewBadgerCache returns the error: the caller is expected to fall back to MemoryCache rather than fail startup entirely — see that type's own doc.
func (*BadgerCache) Close ¶
func (c *BadgerCache) Close() error
Close closes the underlying badger database, flushing any pending writes. Safe to call once during shutdown; badger itself tolerates a repeated Close as a no-op.
func (*BadgerCache) Delete ¶
func (c *BadgerCache) Delete(_ context.Context, key string) error
Delete removes key. It is not an error for key to already be absent — badger's own Txn.Delete has the identical contract.
func (*BadgerCache) Get ¶
Get returns a copy of the value stored under key, or ok=false when the key does not exist or has expired (badger enforces TTL expiry itself — an expired key behaves exactly like an absent one to Get, per badger's own contract for a WithTTL entry).
func (*BadgerCache) RunGC ¶
func (c *BadgerCache) RunGC(ctx context.Context, interval time.Duration)
RunGC runs a value-log GC pass (RunValueLogGCOnce) on a ticker until ctx is cancelled. interval controls how often a pass is attempted. Intended to run in its own goroutine for the process lifetime, following the same signal-cancelled-ctx shutdown pattern the caller's other background workers use.
func (*BadgerCache) RunValueLogGCOnce ¶
func (c *BadgerCache) RunValueLogGCOnce(discardRatio float64) error
RunValueLogGCOnce runs badger's value log garbage collection (DB.RunValueLogGC) repeatedly at discardRatio until nothing more qualifies for reclaim, following badger's own documented retry-while-nil pattern: RunValueLogGC returning nil means a value log file WAS rewritten and reclaimed, so it is called again immediately in case another file also qualifies. Returns badger's own final stopping error (typically ErrNoRewrite) — callers that only care whether a GC pass ran, not the specific reason it stopped, can safely ignore it (see RunGC's own use). Exposed as its own method, not only reachable through RunGC's ticker loop, so a caller — a future admin action, or a test — can trigger a GC pass on demand without waiting out a real interval.
func (*BadgerCache) Set ¶
Set stores value under key. A positive ttl is applied via badger's own NewEntry(...).WithTTL — badger expires and reclaims the entry itself; a zero ttl stores the entry with no expiration. A negative ttl returns ErrNegativeTTL. A positive ttl under badgerMinTTL is rounded up to it (see that constant's own doc) rather than honored at sub-second precision.
type Cache ¶
type Cache interface {
// Get returns the cached value for key. ok is false when the key does
// not exist OR has expired; callers must treat both the same way (a
// cache miss), never distinguish them.
Get(ctx context.Context, key string) (value []byte, ok bool, err error)
// Set stores value under key, expiring it after ttl. A zero ttl means
// "no expiration" — almost every derived/external cache entry should
// instead carry a bounded TTL, so a stale value is never served
// indefinitely once its source of truth has moved on. A negative ttl
// returns ErrNegativeTTL rather than being treated as "no expiration."
Set(ctx context.Context, key string, value []byte, ttl time.Duration) error
// Delete removes key. It is not an error for key to already be
// absent.
Delete(ctx context.Context, key string) error
}
Cache is the port every cache consumer depends on.
Key namespace convention: every key SHOULD be namespaced "<domain>:<purpose>:<id>" (e.g. "recipes:externalfind:<sha256 hex>") so consumers from different bounded contexts never collide on a bare key, and so a key's owner and purpose are obvious from a cache dump or a metrics label alone. This is a naming convention callers are expected to follow, not something the port itself validates — Set does not reject an unnamespaced key.
TTL precision: callers must not rely on sub-second TTL accuracy. MemoryCache preserves the full time.Duration, but BadgerCache honors TTL only to whole-second resolution (badger's own WithTTL truncates to whole Unix seconds) and rounds a positive sub-second ttl up to one second rather than let it appear already-expired the instant it is set. Every real caller of this port so far uses TTLs measured in hours, so this only matters if a future caller passes a sub-second value.
type MemoryCache ¶
type MemoryCache struct {
// contains filtered or unexported fields
}
MemoryCache is an in-process Cache: a mutex-guarded map, with expiry checked lazily at Get (there is no background sweep of expired entries — an entry that is set and never read again simply occupies memory until the process exits, which is acceptable for its two intended uses: hermetic tests, and the caller's own boot-time fallback when BadgerCache fails to open even after its own corruption-recovery retry (see that type's own doc) — a short-lived degraded mode, not a long-running production configuration.
func NewMemoryCache ¶
func NewMemoryCache() *MemoryCache
NewMemoryCache constructs an empty MemoryCache, ready to use immediately — it takes no dependencies and cannot fail to construct.
func (*MemoryCache) Delete ¶
func (c *MemoryCache) Delete(_ context.Context, key string) error
Delete removes key. It is a no-op, not an error, when key is already absent.
func (*MemoryCache) Get ¶
Get returns a COPY of the stored value, so a caller mutating the returned slice can never corrupt what MemoryCache holds internally. An expired entry is lazily deleted the first time it is observed via Get, rather than left to accumulate — a Get scans, at most, the one key requested, so this costs nothing extra per call.