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:
@@ -16,8 +16,33 @@ The two layers are orthogonal — the moka caches shave query round-trips regard
|
||||
| Thumbnail cache | configurable | 1 000 | Generated WebP/AVIF thumbnails |
|
||||
| Image transcode | configurable | 500 | On-the-fly image transcoding results |
|
||||
| Blob hash | 30 s TTI | 5 000 | BLAKE3 hashes for dedup lookups |
|
||||
| Attached blob | 60 s TTL | 50 000 | `file_attached_blobs` row lookups on the thumbnail hot path (ETag + tier-2b, also the Nextcloud preview endpoint) |
|
||||
| Audio metadata | — | 2 000 | ID3 tags and duration |
|
||||
|
||||
### The attached-blob cache
|
||||
|
||||
Every thumbnail request pays a `storage.file_attached_blobs` point query
|
||||
before it can even answer "304 Not Modified" — the ETag names the attached
|
||||
blob's hash. A photos grid revalidating 60 thumbnails per visit means
|
||||
60+ point queries per browse. The cache sits in `DedupService` in front of
|
||||
that lookup (`find_attached_blob`), keyed by the row's `(file_id, kind,
|
||||
variant)` primary key, and caches **both directions**: `Some(row)` and
|
||||
`None` (most files have no attached preview, so the negative side is where
|
||||
most of the win is).
|
||||
|
||||
Two rules keep it honest:
|
||||
|
||||
- **DB faults are never cached.** The uncached lookup surfaces errors as
|
||||
`Err`; only a genuine `Ok(None)` fills a negative entry. A transient
|
||||
outage must not freeze "no attached blob" into place for a full TTL —
|
||||
a read failure is never proof that data is absent.
|
||||
- **TTL is the bound, not the invalidation strategy.** Writes invalidate
|
||||
eagerly — `store_attached_blob` / `store_attached_blob_if_absent` on
|
||||
success, deletions via `ThumbnailRefreshHook::on_file_deleted` (which
|
||||
all three production delete paths fire). The 60 s TTL only bounds what
|
||||
the process cannot see: bare SQL, the `copy_file_satellites` race
|
||||
window, a hypothetical second instance.
|
||||
|
||||
### How it works
|
||||
|
||||
1. **Read path:** check cache → if hit, return immediately (sub-ms); if miss, query PostgreSQL, populate cache, return
|
||||
|
||||
Reference in New Issue
Block a user