arr-meta response cache is unbounded and in memory #158

Closed
opened 2026-08-24 07:45:51 +01:00 by naps62-yolo · 1 comment
Owner

arr-meta's response cache holds every TMDB body in memory as an Arc<str>,
and only ever drops an entry when someone reads a stale one or calls clear().
A body nobody asks for again is never evicted. There is no size bound.

Operator decision: this should be a filesystem cache, not an in-memory one.

The rich metadata milestone made the cost visible. Before it, the cache held
search pages and the reduced movie/series records. It now also holds full
append_to_response=credits,videos,external_ids payloads — several times
larger — one per title anyone opens. Footprint therefore scales with how many
titles get looked at in a day, with no ceiling.

Measured on arr.n62.casa, from Dokploy's samples:

2026-08-24   48.77 MiB
2026-08-23   16.1–16.5 MiB

The gap spans both the TV tracking and Rich metadata milestones, so it is not
all attributable to the cache. But §1 targets 20–40 MB resident and the service
is now above that band, with the one component that has no upper bound sitting
in the middle of it.

Move the cache to disk:

  • A cache directory alongside the database, from bootstrap config (§10), not a
    policy row.
  • Keep the logical request key. The current cache is deliberately keyed on a
    logical key rather than the URL, so the API key never becomes part of a cache
    key — see the module comment in cache.rs. A filename derived from that key
    must preserve the property. Hash it; do not embed the query string.
  • Write atomically (temp file plus rename), so a killed process cannot leave a
    half-written body that parses as valid JSON.
  • Keep the 24h TTL, and add an eviction sweep so expired entries leave without
    waiting to be read. The daily metadata tick (§8) is the obvious lane for it.
  • Bound the directory by size as well as age. Age alone does not stop a busy day
    filling the disk.

A small in-memory layer in front of the files is fine if it is bounded and the
bound is stated. The defect is unboundedness, not memory as such.

DESIGN.md §9.6 says rich detail "is served through arr-meta's existing 24-hour
response cache" — amend that line to say where the cache lives, since it stops
being an implementation detail once it owns disk.

Non-goals: this is not an image cache. §9.6 hotlinks images from
image.tmdb.org and that stays. It also does not move rich detail into the
database — the columns in #145 are still the only persisted metadata.

`arr-meta`'s response cache holds every TMDB body in memory as an `Arc<str>`, and only ever drops an entry when someone reads a stale one or calls `clear()`. A body nobody asks for again is never evicted. There is no size bound. Operator decision: **this should be a filesystem cache, not an in-memory one.** The rich metadata milestone made the cost visible. Before it, the cache held search pages and the reduced movie/series records. It now also holds full `append_to_response=credits,videos,external_ids` payloads — several times larger — one per title anyone opens. Footprint therefore scales with how many titles get looked at in a day, with no ceiling. Measured on `arr.n62.casa`, from Dokploy's samples: ``` 2026-08-24 48.77 MiB 2026-08-23 16.1–16.5 MiB ``` The gap spans both the TV tracking and Rich metadata milestones, so it is not all attributable to the cache. But §1 targets 20–40 MB resident and the service is now above that band, with the one component that has no upper bound sitting in the middle of it. Move the cache to disk: - A cache directory alongside the database, from bootstrap config (§10), not a policy row. - **Keep the logical request key.** The current cache is deliberately keyed on a logical key rather than the URL, so the API key never becomes part of a cache key — see the module comment in `cache.rs`. A filename derived from that key must preserve the property. Hash it; do not embed the query string. - Write atomically (temp file plus rename), so a killed process cannot leave a half-written body that parses as valid JSON. - Keep the 24h TTL, and add an eviction sweep so expired entries leave without waiting to be read. The daily metadata tick (§8) is the obvious lane for it. - Bound the directory by size as well as age. Age alone does not stop a busy day filling the disk. A small in-memory layer in front of the files is fine if it is bounded and the bound is stated. The defect is unboundedness, not memory as such. DESIGN.md §9.6 says rich detail "is served through arr-meta's existing 24-hour response cache" — amend that line to say where the cache lives, since it stops being an implementation detail once it owns disk. Non-goals: this is not an image cache. §9.6 hotlinks images from `image.tmdb.org` and that stays. It also does not move rich detail into the database — the columns in #145 are still the only persisted metadata.
naps62-yolo added the phase/5-uiarea/metadifficulty/moderatetype/bug labels 2026-08-24 07:45:51 +01:00
naps62-yolo added this to the Reliability and first run milestone 2026-08-24 17:58:27 +01:00
Author
Owner

arr-meta's response cache is now disk-backed (crates/arr-meta, reliability/158-bounded-cache, PR pending):

  • Files under a cache_dir, keyed by FNV-1a hash of the logical request key (never the query string or the API key).
  • Atomic writes: temp file + rename.
  • Bounded by age (24h TTL, unchanged) and by total directory size (DEFAULT_CACHE_MAX_BYTES = 64 MiB, sized from the ~32 MiB of resident growth Dokploy measured across the TV-tracking + rich-metadata milestones). Cache::sweep enforces both, runs after every insert, and is exposed as TmdbClient::sweep_cache().
  • TmdbClientBuilder::cache_dir() lets a caller point it at a persistent directory; unset, it falls back to a process-unique temp directory that's removed on drop (same lifetime the old in-memory cache had).
  • DESIGN.md §9.6 and §10 updated to describe the disk cache.

Out of scope here, since arr-daemon is owned by #168/#177 (running in parallel): nothing wires cache_dir() to bootstrap config or calls sweep_cache() from the daily metadata tick (§8) yet. Until that lands, the cache still lives in a temp directory that resets on restart rather than "alongside the database" as the issue asks — functionally bounded, just not yet persistent across restarts. Worth its own follow-up once arr-daemon is free.

arr-meta's response cache is now disk-backed (crates/arr-meta, `reliability/158-bounded-cache`, PR pending): - Files under a `cache_dir`, keyed by FNV-1a hash of the logical request key (never the query string or the API key). - Atomic writes: temp file + rename. - Bounded by age (24h TTL, unchanged) and by total directory size (`DEFAULT_CACHE_MAX_BYTES` = 64 MiB, sized from the ~32 MiB of resident growth Dokploy measured across the TV-tracking + rich-metadata milestones). `Cache::sweep` enforces both, runs after every insert, and is exposed as `TmdbClient::sweep_cache()`. - `TmdbClientBuilder::cache_dir()` lets a caller point it at a persistent directory; unset, it falls back to a process-unique temp directory that's removed on drop (same lifetime the old in-memory cache had). - DESIGN.md §9.6 and §10 updated to describe the disk cache. Out of scope here, since arr-daemon is owned by #168/#177 (running in parallel): nothing wires `cache_dir()` to bootstrap config or calls `sweep_cache()` from the daily metadata tick (§8) yet. Until that lands, the cache still lives in a temp directory that resets on restart rather than "alongside the database" as the issue asks — functionally bounded, just not yet persistent across restarts. Worth its own follow-up once arr-daemon is free.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: yolo/arr#158