P0: implement cache size management and eviction #51
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The disk-backed caches (
src-content,src-metadata,dst-contentunder<statedir>/cache/) grow without bound; a busy or abusive workload can fill the disk. This is the promoted Next Step inTODO.md(after the #49 manual test pass) and a production P0: unbounded disk growth is a denial-of-service vector.Definition of done
cache_max_bytesin the YAML config) with a sane default; the config key naming must follow the existing descriptive-naming conventions.make checkgreen;TODO.mdWorkflow bookkeeping in the finishing commit (promote the next Future Step: config validation on startup); finishing commit title ends with(closes #N)for this issue.Process
Standard workflow: feature branch from
main, implementor works from direction comments on this issue, discussion on the PR, adversarial review,merge-ready+ assign sneak on pass.Owner direction received (supplements the definition of done above; where they differ, this comment wins):
cache_max_bytes, a non-negative integer number of bytes. Validated by the startup validation framework from #52: a SET but invalid value (negative, non-integer, float, null, non-numeric string) aborts startup naming the key and value. The user may set it to ANY non-negative amount — no floor is applied to explicit values.<state_dir>/cache/), measured at startup via statfs on the actual path, with a floor of 500 MiB (524288000 bytes) — i.e.max(0.75 * free_bytes, 500 MiB). The floor applies ONLY to the computed default, never to explicit values. Log the computed effective limit at startup.cache_max_bytes: 0disables the disk cache entirely: no cache reads, no cache writes, no eviction machinery; every request fetches and processes uncached. This is an explicit valid value, not an error.TODO.mdbookkeeping correction to DoD item 5: config validation on startup is already done (#52/#53, merged). This work is the current Next Step; on completion promote the next Future Step (P1 blocked networks configuration) into Next Step per the Workflow section.Standard loop applies: feature branch from current
main, PR labeledneeds-review, adversarial review,merge-ready+ assign sneak on pass. Dispatching an implementer now.Implementation plan (branch
feature/cache-size-evictionfrommainat61f42e6):Config (integrates with the #52/#53 validation framework)
cache_max_bytes(Config fieldCacheMaxBytes int64), added to the known-keys list. StrictgetInt64getter: SET-but-invalid values (negative, float, null, non-numeric string) abort startup naming the key and value. Explicit values take any non-negative amount, no floor.0is valid and disables the disk cache entirely.state_dirvalidation asmax(75% of free bytes on the filesystem containing <state_dir>/cache/, 500 MiB), measured via statfs on the actual cache directory (created first). Free-space probe is injectable so tests do not depend on the host disk. Effective limit logged at startup.Accounting and eviction (in
internal/imgcache)002: newvariant_contenttable (cache_key PK, size_bytes, content_type, created_at, last_accessed_at) so processed variants are DB-tracked like source blobs already are, plus alast_accessed_atcolumn onsource_content; indexes on the LRU timestamps. Total usage = SUM over both tables — no directory scans on the hot path.last_accessed_at(variants on lookup, source blobs on source reuse), same cost class as the existing per-request stats UPDATEs.COALESCE(last_accessed_at, created_at/fetched_at), evicted in batches until usage is under the limit. Rationale in the PR body.source_metadatarows referencing it (plus their JSON sidecar files) and thesource_contentrow in one transaction BEFORE the file is unlinked — a multi-referenced blob is only ever removed together with all its references, and DB rows never point at deleted files.cache_max_bytes: 0: Cache is constructed disabled — no cache directories, lookups always miss, stores and source lookups are no-ops, no evictor. Every request fetches and processes uncached. Note: the negative cache stays active; it is DB-backed (bounded, TTL-expired rows), not part of the disk cache this issue bounds — will flag this for review in the PR.Process
make checkis green; no existing tests modified.config.example.ymland README config list getcache_max_bytes;TODO.mdWorkflow bookkeeping (promote P1 blocked networks into Next Step) in the finishing commit, title ending(closes #51).make check,docker build --target lint ., plus an end-to-end run of the built binary checking the logged default limit, uncached serving withcache_max_bytes: 0, and exit 1 naming the key on an invalid value.Implementation is up as PR #55 (#55), labeled
needs-reviewand assigned to clawbot for the review loop. It follows the plan comment above; deviations and judgment calls, all detailed in the PR body:imgcache.CacheConfiglayer, disabling is an explicitDisableDiskCacheflag instead ofMaxBytes == 0, because pre-existing test fixtures constructCacheConfigwith a zeroMaxBytesand depend on the legacy no-limit behavior (repo rules forbid touching existing tests). The config layer mapscache_max_bytes: 0to the flag; observable behavior matches the direction exactly (verified end-to-end).DisableDiskCache: maxBytes == 0to mirror the production mapping; assertions untouched, no pre-existing tests modified.Also filed #56 for a pre-existing wart found along the way (
Cache.Statsreads never-populated tables).