perf(thumbnail): cache attached-blob lookups on the request path
Every thumbnail request paid an uncached storage.file_attached_blobs point query before it could answer — including 304 revalidations and RAM thumbnail hits, where the ETag path (thumbnail_content_id) probes the row every time and tier 2b probes it again with the same key. A photos grid revalidating 60 thumbnails per visit meant 60+ point queries per browse, repeated on every visit. find_attached_blob now reads through a process-local moka cache in DedupService, keyed by the row's (file_id, kind, variant) PK, holding positive and negative entries (most files have no attached preview, so the negative side carries the win). Two rules keep it honest: - DB faults are surfaced as Err and never cached — a transient outage cannot freeze "no attached blob" into a negative entry (a read failure is never proof that data is absent). The public signature is unchanged; the SQL body moved to find_attached_blob_uncached. - Writes invalidate eagerly: store_attached_blob and the Inserted arm of store_attached_blob_if_absent on success, and deletions via ThumbnailRefreshHook::on_file_deleted, which all three production delete paths (single file, folder cascade, trash clear) fire after the DELETE commits. The 60s TTL bounds only what the process cannot see (bare SQL, copy_file_satellites races). The Nextcloud preview endpoint rides the same lookup and benefits identically. Five in-memory contract tests pin the cache behaviour, including the fault-not-cached rule. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -248,6 +248,19 @@ on `DELETE`. The trigger fires on DELETE only; replacing a preview
|
||||
updates `blob_hash` in place and the Rust path handles that reference
|
||||
swap.
|
||||
|
||||
**Reads are cached; the cache never outlives the truth by design.**
|
||||
`DedupService::find_attached_blob` — the lookup the thumbnail ETag path
|
||||
pays on *every* request, 304 or not — reads through an in-process moka
|
||||
cache keyed by the row's PK, positive and negative entries alike. Two
|
||||
properties make that safe rather than merely fast: a DB fault is
|
||||
surfaced as an error and never fills a negative entry (a failed lookup
|
||||
is not a missing row), and every write path that can change an answer
|
||||
invalidates first — the two `store_attached_blob*` variants on success,
|
||||
deletes via `ThumbnailRefreshHook::on_file_deleted` after the CASCADE
|
||||
committed. The 60 s TTL exists for the residual cases the process
|
||||
cannot observe (bare SQL, `copy_file_satellites` racing a concurrent
|
||||
new file), not as the primary coherence mechanism.
|
||||
|
||||
**Writing a derived row requires its source to exist.**
|
||||
`store_derived_blob` guards the insert with an `EXISTS` on
|
||||
`chunk_manifests`/`blobs`. Without it, a row written just after its
|
||||
|
||||
Reference in New Issue
Block a user