cache

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jul 30, 2026 License: AGPL-3.0 Imports: 8 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
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

func (c *BadgerCache) Get(_ context.Context, key string) ([]byte, bool, error)

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

func (c *BadgerCache) Set(_ context.Context, key string, value []byte, ttl time.Duration) error

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

func (c *MemoryCache) Get(_ context.Context, key string) ([]byte, bool, error)

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.

func (*MemoryCache) Set

func (c *MemoryCache) Set(_ context.Context, key string, value []byte, ttl time.Duration) error

Set stores a COPY of value under key, so a caller mutating the slice it passed in after Set returns can never corrupt what MemoryCache holds internally. A negative ttl returns ErrNegativeTTL.

Jump to

Keyboard shortcuts

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