Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

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 .tgs source; 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 in OQ.md asks 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-168 bounds only the host cache: “LRU … sized by memory not item count” — which bounds RAM and says nothing about disk.
    • No QA gate. QA.md has 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 in PREDICT.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/media is 1 file (stub); ~/dev/fera/fera/tools/bench-scene exists but its main.rs is 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

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, no Resolved marker) 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. Default min(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-46 and OQ.md:719-724 already 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.

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):

  1. Enforcement point: in the media-cache crate, 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.
  2. Budget: min(2 GiB, 10% RAM, 5% free space) default, user configurable, with the settings surface the corpus currently lacks.
  3. Policy: LRU by last-displayed, not last-written; never evict an item currently mmap’d by a live render pass (pin count, refcount by Map handle — cheap and necessary, since the cache is mmap’d and RESEARCH-OQ-003.md is built on that).
  4. 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.
  5. Format decision first, or in the same pass: OQ-003 is still open and TECHSPEC.md:201 still literally reads <raw or per-frame-compressed RGBA frames>. RESEARCH-OQ-003.md already 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, explicit frame_codecs/frame_lengths, 64-byte alignment, and a stated canonical alpha representation.
  6. 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).