feat: cache size management and LRU eviction (closes #51) #55
Reference in New Issue
Block a user
Delete Branch "feature/cache-size-eviction"
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?
Implements #51 per the issue DoD and the owner direction comment (issuecomment-44068).
Behavior
Config:
cache_max_bytes(integrates with the #52/#53 validation framework)getInt64/int64Valgetter in the existing strict-loader pattern; the key is registered in the known-keys list. A SET but invalid value — negative, float, null, non-numeric string, boolean, list — aborts startup with exit 1 naming the key and the offending value.cache_max_bytes: 0is a valid value that disables the disk cache entirely.state_dirvalidation, the default resolves tomax(75% of free bytes on the filesystem containing <state_dir>/cache/, 500 MiB). The cache directory is created first and statfs runs on that actual path, so the measurement hits the right filesystem. The probe is injectable (FreeSpaceProbeFunc) so tests do not depend on the host disk. The effective limit (and disabled state) is logged at startup.Size accounting (no directory scans on the hot path)
002adds avariant_contenttable — processed variants were previously untracked anywhere — and alast_accessed_atcolumn onsource_content, both indexed. Total usage is two SUM queries.Eviction policy: global LRU across both content classes
variant_contentandsource_content(batched, 100 per class per pass, merged oldest-first byCOALESCE(last_accessed_at, fetched_at)), evicted until usage is at or below the limit.source_metadatarows referencing it plus itssource_contentrow in a single transaction BEFORE the file is unlinked. A blob referenced by multiple source paths is only ever removed together with all of its references, and DB rows never point at deleted files (the crash window leaves at worst an orphaned file, which reconciliation sweeps). The JSON metadata sidecars for removed rows are deleted as well.Triggers, off the request path
.metasidecar), drops accounting rows whose files are missing, removes source blob files the DB does not know (unreachable, since lookups go throughsource_metadata), removes rows whose files are gone, and sweeps.tmp-*files older than an hour.cache_max_bytes: 0disables the disk cacheStoreSource/StoreVariantare no-ops, no evictor runs; every request fetches and processes uncached. Verified end-to-end (below).Notes for review
imgcache.CacheConfiglayer, disabling is an explicitDisableDiskCacheflag rather thanMaxBytes == 0, because existing test fixtures constructCacheConfigwithoutMaxBytesand rely on the legacy "no limit" behavior; per repo rules those tests were not touched. The config layer mapscache_max_bytes: 0to the flag inhandlers.MaxBytes == 0at that layer means "no limit enforced" and is unreachable from production config (the computed default is always at least 500 MiB).newEvictionTestCache) to passDisableDiskCache: maxBytes == 0, mirroring the production mapping, when the flag design emerged. Assertions were not touched; no pre-existing tests were modified..meta, metadata JSON) are not counted in usage; they are bounded by entry counts and small (tens of bytes to ~1 KiB per entry) while content bytes dominate. Documented here for transparency.Cache.Statsreads the never-populatedoutput_content/request_cachetables, soTotalItems/TotalSizeBytesare always 0. Out of scope here; filing as a separate issue.Verification
3963ec3adds the failing tests first (18 new tests covering strict parsing, default computation with injected probe including floor and 75% branches, explicit-no-floor, zero-disables, size accounting, dedup accounting, LRU order, multi-reference blob eviction with the no-dangling-references invariant, under-limit no-op, write-pressure trigger, periodic trigger, reconciliation); implementation follows in8cb09b6/bdd86a4until green.make checkgreen (all tests, lint 0 issues, fmt-check) at HEAD.docker build --target lint .green (golangci-lint v2.10.1).computed default cache size limit from free spaceandeffective cache size limit(75% of the test host's free space);cache_max_bytes: banana: exit 1 withconfig key "cache_max_bytes": value "banana" is not an integer;cache_max_bytes: 0:cache_disabled=truelogged, two identical requests both fetch upstream (2 upstream fetches logged), 200image/jpegresponses, nocache/directory created, onlystate.sqlite3in the state dir;variant_contentandsource_contentrows match the on-disk file sizes.Built and verified as described in the PR body. Summary of what was done and how it was checked:
Commits (branch
feature/cache-size-evictionfrommainat61f42e6, headc1ec038):3963ec3— red phase: 18 failing tests (config parsing/default/floor rules, size accounting, LRU eviction, multi-reference blob safety, zero-disables, write-pressure and periodic triggers, reconciliation) plus minimal API skeletons so the tree compiles and lints; verified at that commit that ONLY the new tests failed.8cb09b6—cache_max_bytesconfig key: strictgetInt64getter, known-keys registration, non-negative validation, statfs-derived default with injectable probe, effective-limit logging.bdd86a4— migration 002 (variant_content+source_content.last_accessed_at), usage accounting, global-LRU background evictor with write-pressure and periodic triggers, transactional reference-safe source blob eviction, startup reconciliation,DisableDiskCachemode, handlers wiring (start on OnStart, stop on OnStop).c1ec038— docs (config.example.yml, README key list) andTODO.mdWorkflow bookkeeping (P1 blocked networks promoted to Next Step).Verification:
make checkgreen at HEAD (all tests, golangci-lint 0 issues, fmt-check);docker build --target lint .green against the pinned CI golangci-lint v2.10.1; end-to-end runs of the builtpixadconfirming the computed default is logged, an invalidcache_max_bytesexits 1 naming key and value,cache_max_bytes: 0serves every request uncached with no cache directory created, and the enabled path serves the second request from cache with accounting rows matching on-disk sizes.Discovered issue filed separately: #56 (
Cache.Statsreads the never-populatedoutput_content/request_cachetables).View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.