cache

package
v0.2.3 Latest Latest
Warning

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

Go to latest
Published: Jul 2, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

Documentation

Overview

Package cache implements a two-tier, bounded, sharded-LRU object cache for the cadish HTTP cache server.

Tiers and routing

Objects live in one of two tiers, both safe for concurrent use:

  • RAM tier (ram.go): in-memory, for small hot objects (HLS .m3u8 playlists, images). Bounded by a total byte budget plus a per-object buffer cap and a process-wide in-flight buffering budget (the "B2" OOM guard, below).
  • Disk tier (disk.go): NVMe-backed, for large media (.mp4, .ts segments). Blob content lives under <dir>/blobs/<aa>/<bb>/<sha256(key)>; the index (metadata + LRU order) is persisted to per-shard files (<dir>/index-<n>.json) so the cache survives a restart.

Store (router.go) is the facade the server consumes. It owns both tiers and a RouterConfig, and on each write picks a tier via pickTier(key, size):

  • Keys with an always-RAM extension (.m3u8 .jpg .jpeg .webp .png .gif) go to RAM — UNLESS their size is known and exceeds the per-object RAM cap (RAMMaxObjectBytes), in which case they go to disk so a giant mislabeled playlist/image is still cached somewhere instead of dropped. Unknown size (-1) still starts in RAM, protected by the bounded writer.
  • Any other key whose size is known and <= SmallObjectThreshold goes to RAM.
  • Everything else (large, or unknown size) goes to disk.

Reads (Store.Get / Store.GetTier) check RAM first, then disk, returning the first hit and (for GetTier) which tier served it for per-tier hit-rate metrics.

Sharding and eviction

Each tier is split into shardCount(maxBytes) independent shards (shard.go), up to 64, each with its own mutex, map, LRU list and byte counter, and an equal slice of the tier budget (shardCaps sums to exactly maxBytes). A key always maps to the same shard via FNV-1a hash, so the hot read path — a cache hit does an LRU MoveToFront, i.e. a WRITE under the lock — only contends with other keys on the SAME shard rather than one global mutex. Eviction is therefore per-shard approximate-LRU: when a shard is full, it evicts its own least-recently-used entries until the new object fits. A small/test-sized budget (<= 1 MiB) collapses to a single shard, preserving exact global-LRU semantics. An object larger than its shard's cap is refused (streams through uncached) and never wedges the shard. Per-shard byte counts are mirrored into atomics so Bytes()/Stats() aggregate without taking any shard lock.

RAM OOM guard (B2)

The RAM tier bounds writes on two PROCESS-WIDE axes (never sharded) so it can never be driven to OOM: a per-object buffer cap (RAMMaxObjectBytes) and a total in-flight buffering budget across all active writers (RAMInflightBudget). A write that would exceed either makes that writer "overflow" — it frees its buffer and streams the body to the client uncached, never committing — so the client stream is never disturbed.

Persistence and restart

Disk index persistence is decoupled from the commit hot path AND partitioned per shard: commits and evictions set a per-shard dirty flag and a single background goroutine flushes at most once per persistInterval (5s), debounced, rewriting only the DIRTY shards' index files (<dir>/index-<n>.json) — clean shards are never walked or rewritten — plus a final synchronous flush of all shards on Close. Each index file carries a CRC over its body, so a torn (power-loss) write drops only that shard's entries instead of the whole index. On load() every per-shard file is read/validated in parallel and its entries re-homed to the current shard by hash (so a shard-count change still loads) and re-validated against the blob's on-disk size; a missing or size-mismatched blob is dropped. A legacy single index.json is migrated to per-shard files on first boot. A crash between flushes costs at most a re-fetch, never a stale or corrupt hit. PersistErrors counts failed flushes.

Not in this package

Request coalescing (single-flight on misses) lives in cadish's server layer, not here — this package is purely the cache store. The server is expected to wrap Store.Get/Writer with that mechanism.

Each tier implements the Tier interface. Tiers are independently bounded and LRU-evicted.

Index

Constants

View Source
const (
	TierNone = ""
	TierRAM  = "ram"
	TierDisk = "disk"
)

Tier source identifiers returned by GetTier (for hit-rate metrics).

Variables

View Source
var ErrNotFound = errors.New("cache: not found")

ErrNotFound is returned by Get when the key is absent from the tier.

Functions

This section is empty.

Types

type DiskTier

type DiskTier struct {
	// contains filtered or unexported fields
}

DiskTier is an NVMe-backed, byte-bounded, LRU cache for large media objects. Content lives under <dir>/blobs/<aa>/<bb>/<sha256(key)> (two-level fan-out by the first two hex bytes of the hash — see blobPath); in-progress writes stream through <dir>/blobs/wip/ before an atomic same-filesystem rename into place. Metadata + LRU order are persisted to per-shard index files (<dir>/index-<n>.json) so the cache survives a restart (a legacy single <dir>/index.json is migrated on first boot).

SHARDING: the tier is split into shardCount(maxBytes) independent shards, each with its own mutex/map/LRU/counter and an equal slice of maxBytes (shardCaps). A key always maps to the same shard (shardIndex over the tier's shard count), so the hot read path (a cache hit does an LRU MoveToFront, i.e. a WRITE) only contends with other keys on the SAME shard instead of one global mutex. Eviction is therefore per-shard approximate-LRU (see shardCaps for the accepted consequence). The on-disk blob layout is UNCHANGED — sha256(key) filenames — so sharding is invisible on disk; only the in-memory index is partitioned.

Index persistence is decoupled from the commit hot path AND partitioned per shard (CAD-47). At ~2 TB/day the disk tier sees a high commit rate; marshalling the whole index and renaming it under a lock on EVERY commit (the original design) is O(n) work plus lock contention on every cached object. Instead, commits/evictions set a per-shard dirty flag and ONE background goroutine flushes at most once per persistInterval (debounced), plus a final synchronous flush on Close. The index is now ONE FILE PER SHARD (<dir>/index-<n>.json): the flusher (re)writes a shard's file ONLY when that shard is dirty, copying its LRU under a short lock and marshalling+writing OUTSIDE the lock, so flush cost is proportional to the DIRTY-shard count — clean shards are never walked and never rewritten. Each file carries a header with a CRC over its JSON body, so a torn (power-loss) write drops only THAT shard's entries instead of poisoning the whole index. On load() every per-shard file is read/validated in parallel and its entries re-homed to the CURRENT shard by hash (so a shard-count change across restarts still loads correctly, exactly as the old flat list did). A legacy single index.json is migrated to per-shard files on first boot and then removed (that removal is the migration commit point).

func NewDiskTier

func NewDiskTier(dir string, maxBytes int64) (*DiskTier, error)

NewDiskTier opens (or creates) a disk tier rooted at dir, bounded to maxBytes. It loads any previously persisted index, dropping entries whose blob is missing.

func (*DiskTier) Bytes

func (d *DiskTier) Bytes() int64

Bytes sums each shard's lock-free atomic byte counter, so it does NOT acquire any shard lock and never contends with the hot Get/commit paths.

func (*DiskTier) Close

func (d *DiskTier) Close() error

Close stops the background flusher and writes the index one last time so a graceful shutdown loses nothing. Safe to call multiple times. The stop/done channels are never reassigned, so the flusher's reads of them never race.

func (*DiskTier) Delete added in v0.2.1

func (d *DiskTier) Delete(key string)

Delete removes key from its shard (entry, blob and byte accounting) if present, no-op otherwise. Used for cross-tier dedup by the Store. It marks the shard dirty (via removeLocked) so the corrected index is persisted.

func (*DiskTier) Get

func (d *DiskTier) Get(key string) (*Reader, bool)

func (*DiskTier) Len

func (d *DiskTier) Len() int

func (*DiskTier) NoTierDiscards added in v0.2.1

func (d *DiskTier) NoTierDiscards() int64

NoTierDiscards returns how many objects were refused at commit because this tier has no budget at all (a RAM-only deployment). A nonzero and growing value means the origin is serving chunked/unknown-length (or over-RAM-threshold) responses that the automatic size policy routes to the absent disk tier, so they are cached NOWHERE — add a `disk` tier (cache { disk … SIZE }) or ensure the origin sends a small Content-Length so the response can live in RAM.

func (*DiskTier) OversizeDiscards

func (d *DiskTier) OversizeDiscards() int64

OversizeDiscards returns how many objects were refused at commit for exceeding their shard's cap (and thus cached nowhere, streamed through uncached). A nonzero and growing value means the disk tier is too small for the objects being served — raise DiskMaxBytes or route those objects elsewhere (F6 observability).

func (*DiskTier) PersistErrors

func (d *DiskTier) PersistErrors() int64

PersistErrors returns how many background index flushes have failed. A nonzero and growing value means the disk index is going stale (the cache still serves correctly, but more would be lost on a crash) — worth alerting on.

func (*DiskTier) Reset added in v0.2.1

func (d *DiskTier) Reset()

Reset drops every cached object from the disk tier: it removes all blob files, clears each shard's in-memory state, and persists an EMPTIED index so a later restart reloads nothing. It is the reload path's fail-safe flush when a site's cache-key scheme changed (config.TransplantStoresFrom): a freshly-opened cold store reloads the previous run's on-disk blobs, but those are keyed under the OLD recipe and must not be served for a key that now addresses different content. Callers invoke it on a store that is not yet serving; it is shard-locked regardless so it stays safe against the background flusher.

func (*DiskTier) SetLogger

func (d *DiskTier) SetLogger(log *slog.Logger)

SetLogger attaches an optional observability logger. Nil keeps the tier silent (the default). Used by the server to surface the per-shard-cap oversize discard (F6) without making the cache package own a logger. It is an atomic store (R33) so a reload re-attaching the logger (attachStoreLoggers) races neither a concurrent oversize commit's read nor another reload.

func (*DiskTier) Writer

func (d *DiskTier) Writer(meta ObjectMeta) (TierWriter, error)

type ObjectMeta

type ObjectMeta struct {
	Key          string `json:"key"`
	Size         int64  `json:"size"`
	ContentType  string `json:"content_type"`
	ETag         string `json:"etag,omitempty"`
	LastModified string `json:"last_modified,omitempty"`
	// ContentEncoding is the stored representation's Content-Encoding (e.g. "gzip" when the
	// origin compressed the body and cadish cached the compressed bytes). It MUST be replayed
	// on every HIT — a cached encoded body served without its Content-Encoding header is an
	// undecodable/corrupt response (RFC 9110 §8.4: the encoding is representation metadata).
	// Empty for the common identity body.
	ContentEncoding string `json:"content_encoding,omitempty"`
	// Vary is the origin's Vary header (when present). cadish partitions its OWN cache by the
	// cache key (EvalResponse only stores a response whose Vary the key covers), but a HIT must
	// still re-emit Vary so a DOWNSTREAM shared cache — or the Cadish Edge tier in front — keeps
	// the variance signal and does not serve one variant to a client needing another (RFC 9111
	// §4.1). Empty when the origin sent no Vary.
	Vary string `json:"vary,omitempty"`
	// Status is the cached HTTP response status. The common positive case (200)
	// is left zero and persisted compactly via omitempty; a NEGATIVE cache entry
	// (e.g. a cached 404/410 for a deleted object, `cache_ttl status 404 410 …`)
	// records its status here so the hit is served with the right code. A zero
	// value means 200 — see EffectiveStatus — which keeps pre-existing index
	// entries (written before this field existed) serving as 200 on reload.
	Status int `json:"status,omitempty"`
	// ForcedPrivate records that this object was stored ONLY because `cache_unsafe`
	// overrode an origin Cache-Control marking it unshareable (private/no-store/no-cache/
	// s-maxage=0). On a HIT the server must advertise `private, max-age=N` downstream
	// (not `public`) so a CDN / browser / the Edge tier in front does not cache a response
	// the origin marked confidential (R13/D96). The original origin Cache-Control is not
	// otherwise replayed on a HIT, so this one bit carries the "don't say public" signal.
	// Zero/false (the common case + legacy index entries) ⇒ the normal `public, max-age=N`.
	ForcedPrivate bool `json:"forced_private,omitempty"`
	// Tier is an OPTIONAL placement override: "ram" or "disk" forces the object
	// into that tier instead of the automatic size-based routing. It carries a
	// `storage <selector> -> ram|disk` decision from the pipeline. Empty ("") means
	// "route automatically" (the default). It is a write-time hint and is not
	// persisted — on reload, the object lives in whichever tier's index holds it.
	Tier string `json:"-"`
}

ObjectMeta is the metadata persisted alongside a cached object so that cache hits can be served (and the cache survives a restart) without contacting the origin. It carries everything needed to reconstruct the HTTP response headers.

func (ObjectMeta) EffectiveStatus

func (m ObjectMeta) EffectiveStatus() int

EffectiveStatus returns the response status to serve for this object, mapping the zero value to 200 (positive cache entry / legacy index entry).

type RAMTier

type RAMTier struct {
	// contains filtered or unexported fields
}

RAMTier is an in-memory, byte-bounded, LRU cache for small hot objects.

SHARDING: the tier is split into shardCount(maxBytes) independent shards (shard.go), each with its own RWMutex/map/ring/counter and an equal slice of maxBytes. A key always maps to the same shard (shardIndex). The hot read path takes the shard RLock (shared) and only sets an atomic CLOCK ref bit, so it neither serializes concurrent hits to one hot key nor contends across keys on the same shard. Eviction is per-shard approximate-LRU via the CLOCK / second-chance algorithm (see commit + shardCaps for the accepted consequence).

B2 OOM guard (UNCHANGED by sharding — these bounds stay PROCESS-WIDE on the tier, never sharded): writes are bounded on TWO axes so the RAM tier can never be driven to OOM by one huge object or many concurrent large ones.

  • maxObjectBytes is a per-object buffer cap; a ramWriter that would exceed it overflows (frees its buffer, discards the rest, never commits) so the object streams to the client uncached instead of being buffered whole.
  • inflightBudget bounds the TOTAL bytes all active ramWriters may buffer at once (an atomic byte budget, inflightBytes <= inflightBudget). A write that would push the global total over budget makes that writer overflow too. This caps concurrent buffering even when each individual object is under the per-object cap. Keeping it process-wide (one atomic on the tier, shared by all shards) is REQUIRED: it bounds total live buffering across the whole process, which a per-shard budget could not do.

func NewRAMTier

func NewRAMTier(maxBytes, maxObjectBytes, inflightBudget int64) *RAMTier

NewRAMTier creates an in-memory tier bounded to maxBytes, with a per-object buffer cap (maxObjectBytes) and a process-wide in-flight buffering budget (inflightBudget), both part of the B2 OOM guard. A non-positive maxObjectBytes/inflightBudget disables that particular bound (kept permissive so existing callers/tests that construct a tier directly are unaffected). The maxBytes budget is split equally across the shards (shardCaps), so the sum of per-shard caps stays <= maxBytes.

func (*RAMTier) Bytes

func (r *RAMTier) Bytes() int64

Bytes sums each shard's lock-free atomic byte counter, so it does NOT acquire any shard lock and never contends with the hot Get/commit paths.

func (*RAMTier) Close

func (r *RAMTier) Close() error

func (*RAMTier) Delete added in v0.2.1

func (r *RAMTier) Delete(key string)

Delete removes key from its shard if present (no-op otherwise), mirroring the replace path in commit. Used for cross-tier dedup by the Store.

func (*RAMTier) Get

func (r *RAMTier) Get(key string) (*Reader, bool)

func (*RAMTier) Len

func (r *RAMTier) Len() int

func (*RAMTier) Reset added in v0.2.1

func (r *RAMTier) Reset()

Reset drops every committed object from the RAM tier (clearing each shard's map, LRU ring and byte count). It does NOT touch the in-flight reservation budget, which belongs to live writers, not committed objects. Used by Store.Reset on the reload flush path against a store that is not yet serving; a freshly-opened store's RAM is already empty (RAM is never persisted), so this is normally a no-op kept for completeness and robustness.

func (*RAMTier) Writer

func (r *RAMTier) Writer(meta ObjectMeta) (TierWriter, error)

type Reader

type Reader struct {
	Meta ObjectMeta
	io.ReadCloser
}

Reader is an object's content reader plus its metadata. Callers MUST Close it.

func (*Reader) Close added in v0.2.3

func (r *Reader) Close() error

Close closes the underlying content reader and returns the wrapper to the pool. It overrides the ReadCloser's promoted Close so the wrapper is recycled. Per the io.ReadCloser contract it must be called exactly once; a second call is a no-op (it never double-returns a wrapper to the pool, which would be a use-after-free).

func (*Reader) WriteTo

func (r *Reader) WriteTo(w io.Writer) (int64, error)

WriteTo lets io.Copy hand the whole object off to the destination in one shot when the underlying content reader supports it, avoiding io.Copy's per-call 32 KiB scratch-buffer allocation on the HIT serve path. Embedding the io.ReadCloser *interface* only promotes Read/Close — WriterTo is not in that method set, so without this delegator io.Copy(dst, reader) falls back to the buffered loop (allocating a 32 KiB buffer every HIT) even for a fast source.

DISK TIER — sendfile: the content reader is a real *os.File. *os.File.WriteTo(w) can only splice (sendfile/copy_file_range) into ANOTHER *os.File; into a socket it degrades to a userspace read→write loop (file→user→socket double copy). So instead of delegating to os.File.WriteTo, we hand the file descriptor to a destination that implements io.ReaderFrom (net/http's *response does) via w.ReadFrom(file): that resolves to (*net.TCPConn).ReadFrom → sendfile(2), a kernel file→socket splice with no userspace buffering. The *os.File must reach the destination UN-WRAPPED for net/http to recognize it and engage sendfile; we pass the concrete *os.File, so it does. When the destination is NOT a ReaderFrom (a test double, a wrapper that can't splice) we fall through to os.File.WriteTo's buffered loop below — correct, just not zero-copy.

RAM TIER: the content reader is a *bytes.Reader whose WriteTo writes the whole slice to w in a single Write (no buffer, no per-byte codec) — already optimal, and it can't be a file, so it keeps the WriterTo hand-off.

type RouterConfig

type RouterConfig struct {
	// RAMMaxBytes bounds the in-memory tier (small hot objects).
	RAMMaxBytes int64
	// DiskMaxBytes bounds the NVMe tier (large media).
	DiskMaxBytes int64
	// DiskDir is the directory backing the disk tier.
	DiskDir string
	// SmallObjectThreshold: objects whose size is known and <= this go to RAM
	// (in addition to always-RAM extensions). Larger objects go to disk.
	SmallObjectThreshold int64
	// RAMMaxObjectBytes is the PER-OBJECT cap on the RAM tier (B2). A ram-extension
	// (.m3u8/.jpg/…) whose size is KNOWN and exceeds this is routed to DISK by
	// pickTier instead of RAM; an unknown-size ram-extension still starts in RAM but
	// is bounded by the ramWriter, which overflows (streams through, drops the cache)
	// once buffering would exceed this cap. This stops a single huge object from
	// OOM-killing the box by being buffered whole before the old commit-time size
	// check. Resolved to a sane default (<= RAMMaxBytes) in NewStore when zero.
	RAMMaxObjectBytes int64
	// RAMInflightBudget bounds the TOTAL bytes all active ramWriters may buffer at
	// once (B2). Each ramWriter charges this process-wide atomic budget as its buffer
	// grows and releases on Commit/Abort; a charge that would exceed the budget makes
	// that writer overflow (stream-through, no cache) instead of allocating. This caps
	// concurrent RAM buffering so N simultaneous large writes cannot collectively OOM,
	// even though each one is individually under RAMMaxObjectBytes. Resolved to a sane
	// default (RAMMaxBytes/4) in NewStore when zero, and clamped to <= RAMMaxBytes so an
	// explicit (or stale) budget can never be looser than the committed tier itself —
	// which would let transient in-flight buffering roughly double peak RAM and defeat
	// the OOM guard.
	RAMInflightBudget int64
	// TierExtensions is the per-extension default placement from
	// `cache { tier .ext… -> ram|disk }`: a lower-cased extension (".mp4") maps to
	// "ram"/"disk". It is a DEFAULT — a per-request `storage … -> tier` rule
	// (ObjectMeta.Tier) overrides it. nil/empty means "use the built-in size
	// policy". Honored with the same safety fallbacks as an explicit override.
	TierExtensions map[string]string
}

RouterConfig controls how objects are routed between the RAM and disk tiers.

func DefaultRouterConfig

func DefaultRouterConfig(diskDir string) RouterConfig

DefaultRouterConfig returns sensible defaults for a 64 GB RAM / 400 GB NVMe box (the b3-64 the fleet runs on): ~44 GB RAM tier, ~350 GB disk tier, 2 MB small-object threshold.

type Stats

type Stats struct {
	RAMObjects, DiskObjects int
	RAMBytes, DiskBytes     int64
	RAMMaxBytes             int64
	DiskMaxBytes            int64
	DiskPersistErrors       int64
	DiskOversizeDiscards    int64
	// DiskNoTierDiscards counts responses cached NOWHERE because this is a RAM-only
	// deployment (no disk tier) yet the automatic size policy routed a chunked/
	// unknown-length (or over-threshold) response to the absent disk tier. A growing
	// value means a dynamic/chunked origin is getting zero caching — add a `disk` tier
	// or have the origin send a small Content-Length. Distinct from DiskOversizeDiscards.
	DiskNoTierDiscards int64
}

Stats reports per-tier usage (for /healthz, /stats and logging).

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store is the two-tier cache facade used by the server. It owns both tiers and routes reads/writes to the appropriate one.

func NewStore

func NewStore(cfg RouterConfig) (*Store, error)

NewStore constructs both tiers from cfg.

func (*Store) Close

func (s *Store) Close() error

Close flushes both tiers (persisting disk metadata).

func (*Store) DiskDir

func (s *Store) DiskDir() string

DiskDir is the directory backing this store's disk tier. It lets a caller (the reload path) tell whether a temp directory is still in use by a preserved store before removing it.

func (*Store) Get

func (s *Store) Get(key string) (*Reader, bool)

Get looks the key up in both tiers (RAM first), returning the first hit.

func (*Store) GetTier

func (s *Store) GetTier(key string) (*Reader, string, bool)

GetTier is like Get but also reports which tier served the hit ("ram"/"disk"), so the caller can attribute cache hit-rate per tier without a second lookup.

func (*Store) Reset added in v0.2.1

func (s *Store) Reset()

Reset drops every cached object from BOTH tiers and persists an emptied disk index, returning the store to a cold state. It is the reload path's fail-safe FLUSH when a site's cache-key scheme changed (config.TransplantStoresFrom): a freshly-opened cold store reloads the previous run's on-disk blobs, which are keyed under the OLD recipe, so reusing them could serve a wrong (cross-content) object for a key that now addresses different content. Call it before the store serves any traffic.

func (*Store) SetLogger

func (s *Store) SetLogger(log *slog.Logger)

SetLogger attaches an optional observability logger to the disk tier so a per-shard-cap oversize discard (an object too big for the disk tier, cached nowhere) is logged (F6). Nil keeps the tier silent (the default). A RAM-only or non-DiskTier store is a no-op. Call once at setup, before serving traffic.

func (*Store) Stats

func (s *Store) Stats() Stats

func (*Store) Writer

func (s *Store) Writer(meta ObjectMeta) (TierWriter, error)

Writer returns a TierWriter for meta's tier: an explicit meta.Tier override (from a `storage … -> ram|disk` rule) when set, otherwise automatic size-based routing.

CROSS-TIER DEDUP (R14): a key's destination tier is size-dependent, so a re-store of the SAME key can route to a DIFFERENT tier than a prior copy (e.g. first served with a small Content-Length → RAM, later revalidated chunked/unknown-size → disk). GetTier returns the first hit (RAM before disk), so without eviction the stale RAM copy would permanently shadow the fresh disk copy. We therefore wrap the writer so that on a successful Commit the SAME key is deleted from the OTHER tier — guaranteeing a key lives in at most one tier and a GET always returns the most-recently-committed copy.

type Tier

type Tier interface {
	// Get returns a Reader positioned at the start of the object, or ErrNotFound.
	// A successful Get marks the entry most-recently-used.
	Get(key string) (*Reader, bool)

	// Writer returns a WriteCloser that the caller streams the object body into.
	// On Close (without prior abort) the object is committed to the tier and may
	// trigger eviction of older entries. meta.Size may be 0/unknown up front; the
	// tier records the actual bytes written. The caller should call Abort on the
	// returned writer (instead of Close) to discard a partial/failed write.
	Writer(meta ObjectMeta) (TierWriter, error)

	// Delete removes key from the tier (dropping the in-memory entry and, for the
	// disk tier, its blob). It is a no-op when the key is absent. Used by the Store
	// to keep a key in at most ONE tier: when a re-store routes a key to a different
	// tier than a prior copy, the sibling tier's now-superseded copy is deleted so a
	// GET can never serve the stale shadow (cross-tier dedup).
	Delete(key string)

	// Len returns the number of cached objects.
	Len() int

	// Bytes returns the total bytes currently held.
	Bytes() int64

	// Close releases tier resources (flushing disk metadata, etc.).
	Close() error

	// Reset drops every cached object from the tier (and, for the disk tier, removes
	// the blob files and persists an emptied index). Used by the reload flush path
	// (Store.Reset) when a site's cache-key scheme changed, against a store not yet
	// serving traffic.
	Reset()
}

Tier is a single bounded cache layer (RAM or disk).

type TierWriter

type TierWriter interface {
	io.Writer
	// Commit finalizes the object into the tier (updating size to bytes written).
	Commit() error
	// Abort discards the in-progress write and releases any temp resources.
	Abort() error
	// Stored reports whether the most recent Commit ACTUALLY installed the object in
	// the tier. A nil Commit error does NOT imply storage: a RAM overflow (per-object
	// cap / global in-flight budget / shard cap) or a disk oversize discard returns nil
	// without installing anything. Only meaningful after Commit; false before it. The
	// cross-tier dedup (R14) gates the sibling delete on this so an overflowed re-store
	// never destroys the only real cached copy.
	Stored() bool
}

TierWriter receives a streamed object body. Either Commit or Abort must be called exactly once; Abort discards the partial write.

Jump to

Keyboard shortcuts

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