No eviction or quota policy exists for any on-disk cache — the sticker research proves it is a disk-fill incident
Источник:
dev/fera-research/docs/oc1/013-no-media-cache-eviction-or-quota-policy.md(read-only; сгенерировано scripts/sync-docs.mjs)
No eviction or quota policy exists for any on-disk cache — the sticker research proves it is a disk-fill incident
Section titled “No eviction or quota policy exists for any on-disk cache — the sticker research proves it is a disk-fill incident”- kind: problem
- area: storage / perf
- evidence:
RESEARCH-OQ-003.md:46-53— raw 512² RGBA frames are ~90 MiB per sticker (1 MiB/frame × ~90 frames), a ~1440× blowup over the ~64 KiB.tgssource; and the document’s own verdict: “raw @ 512² cannot ship unbounded. It needs a quota + eviction policy.”RESEARCH-OQ-003.md:49— three sticker sets ≈ 32 GiB of cache.RESEARCH-OQ-003.md:372-374— a provisional gate “≤ 2 GiB / 100 stickers @ 512²” is proposed for the benchmark, not as a policy.- The cache directories exist in the canonical tree with no stated
bound:
OQ.md:696-698—cache/media/,cache/stickers/,cache/thumbnails/. - No OQ asks the question.
OQ.md:70-80(OQ-003) asks storage format only (“raw vs. per-frame-compressed”). Nothing inOQ.mdasks about eviction, quota, TTL or disk budget. - No ADR covers it. ADR-005’s host-side cache (
ADR.md:130-133) is explicitly the in-memory LRU in front of the storage module. There is no ADR for on-disk cache retention of any kind. TECHSPEC.md:166-168bounds only the host cache: “LRU … sized by memory not item count” — which bounds RAM and says nothing about disk.- No QA gate.
QA.mdhas zero cache-eviction or disk-budget gates (verified across all 50 items). - Related unbounded stores with no policy either: media blobs (ADR-031
puts them in the content tier,
ADR.md:653-654), the predictor event log (30-day retention exists only inPREDICT.md:283-284/OQ.md:778, not in ADR-019 or TECHSPEC §5), and outbox entries (TECHSPEC.md:118-123). - Repo reality:
~/dev/fera/fera/core/crates/mediais 1 file (stub);~/dev/fera/fera/tools/bench-sceneexists but itsmain.rsis a 15-line skeleton printing"fera-bench-scene: fera-scene-v0 generator skeleton"— the frozen scene (OQ.md:130-131) has no generator, so every perf gate that references it is currently unrunnable.
- severity: high
Суть / Substance
Section titled “Суть / Substance”The corpus contains a quantified warning that its most expensive cache is unbounded, and no decision anywhere about what bounds it. This is the classic shape of a support incident that is fully predicted and fully unowned:
- Every design document that mentions the cache treats it as an implementation detail (format, keys, mmap). None mentions lifetime.
- The one document that did the arithmetic concluded a policy is required
and then stopped, because the OQ it was answering (
OQ-003) was scoped to format. - So the format decision (still open —
OQ.md:70-80, noResolvedmarker) and the policy decision (never asked) are entangled, and the cheap format answer (compressed) makes the quota question more urgent, not less: compression is what makes a large cache affordable, so it invites more of it.
The failure mode is not theoretical for a messenger: caches grow monotonically with usage, users never see them, and the first symptom is “the app is slow / my disk is full” with no in-app explanation and no reclaim path. Nothing in the design has a settings screen for cache size because no ADR ever asked for one.
Secondary but real: the same gap covers cache/media (decoded frames,
thumbnails), and the content tier means the blob store is encrypted, so
a naive “re-download if missing” policy has real bandwidth cost — which is
exactly the trade-off the quota has to price. The predictor log has a
30-day retention rule in PREDICT.md but not in ADR-019, so it is not a
decision anyone will implement.
Варианты решения (tradeoffs)
Section titled “Варианты решения (tradeoffs)”- (A) Quota + LRU by last-display, enforced centrally in
media-cache. Defaultmin(2 GiB, 10% of RAM, 5% of filesystem free), configurable; eviction by last-displayed (not LRU-by-write, since stickers are re-shown); the Turso metadata index is the natural place to answer “which cached items are older than N and unopened” —FUTURE-CHECKS.md:42-46andOQ.md:719-724already assign Turso exactly that query, but Turso has no ADR, no crate and no module home (see my finding 020). Cost: one new decision surface (settings UI, index, enforcement point). - (B) Bounded-by-construction instead of eviction. Store only the most recent N stickers ever (N ≈ 200–500) with no per-user quota maths. Simpler; fails for users with big curated sets; loses the warm-start benefit exactly for the heavy users who need it.
- (C) TTL only (e.g. 30 days). Dead-simple, and matches the predictor log’s existing rule. But TTL ignores the hot set: a sticker used daily is evicted at 30 days regardless, and a 32 GiB burst is still possible within the window.
- (D) Defer to v1.5. Reject: the arithmetic is already done, and the mitigation is a counter plus a policy, not an architecture.
Предлагаемое решение
Section titled “Предлагаемое решение”New ADR-032 “Cache lifecycle: quota, eviction, retention”, decided now, covering all on-disk caches in one place (this is exactly the kind of cross-cutting policy that ADRs are for):
- Enforcement point: in the
media-cachecrate, at the storage boundary — not in the UI, not in the fetch pool — so it applies regardless of which decoder/Feature module produced the bytes. - Budget:
min(2 GiB, 10% RAM, 5% free space)default, user configurable, with the settings surface the corpus currently lacks. - Policy: LRU by last-displayed, not last-written; never evict an
item currently mmap’d by a live render pass (pin count, refcount by
Maphandle — cheap and necessary, since the cache is mmap’d andRESEARCH-OQ-003.mdis built on that). - Interaction with encryption: evicting a content-tier blob means a re-download (bandwidth), so the quota must be reported to the user as “cache size” with the re-download cost visible, and the predictor’s prefetch (§5) must not fight eviction — prefetch writes go through the same quota, at predictor priority.
- Format decision first, or in the same pass: OQ-003 is still open
and
TECHSPEC.md:201still literally reads<raw or per-frame-compressed RGBA frames>.RESEARCH-OQ-003.mdalready pre-registers the default (per-frame zstd + per-frame codec tag with raw fallback + frame dedup + alpha=0 canonicalization + bounded RAM decompress-ahead ring K=2–4); close OQ-003 by measurement against the frozen scene rather than re-deriving, and add the missing spec items the research itself proposed but nobody applied: file magic + format version, explicitframe_codecs/frame_lengths, 64-byte alignment, and a stated canonical alpha representation. - QA gate to add: “the cache never exceeds its configured quota, and eviction is observable in-app” — plus one for “a truncated cache file is detected, not rendered” (the research flags that a corrupt file is currently indistinguishable from a valid one).