feat(thumbnails): read the derived tier ahead of the sidecar

Step 10c. The sidecar is local disk — invisible to other instances,
uncarried by a backend migration, uncovered by any consistency job.
Reading the derived tier first is what makes that state deletable. Not
the cost it appears to be: CachedBlobBackend gives the blob read a local
disk cache and moka absorbs the repeats above it.

Not a two-line swap, for two reasons.

A derived MISS must fall through to the sidecar; the old code
terminated the lookup with `?` because it was last. While the imports
drain, most content has a sidecar and no row — terminating there would
report "no thumbnail" for nearly all of it.

And the derived tier is WebP-only. store_derived_blob writes image/webp
and keys `variant` on the size alone, with no format term, so a JPEG
request matches the WebP row and would be served the wrong codec. The
old ordering hid this because the .jpg sidecar won first. So the lookup
is gated to WebP, JPEG clients stay on the sidecar — and the sidecar
cannot be deleted for them until `variant` encodes format. That is a new
prerequisite for step 10e, recorded in the plan rather than discovered
later.

Also corrects the plan: I had written that this flip removes the
derived-hash ETag hazard. It does not. A first render still creates the
row as a side effect of producing the body, whatever the read order, so
two consecutive reads still straddle its appearance. The real fix is
resolving the ETag after generation on the 200 path — a 304 only fires
when the client already holds a validator, which implies the row exists.
That is a handler restructure, not an ordering change.
This commit is contained in:
Edouard Vanbelle
2026-08-26 22:51:30 +02:00
parent 48d9e164d4
commit 260e6bb74f
2 changed files with 74 additions and 32 deletions
+30 -7
View File
@@ -1003,9 +1003,19 @@ first render was immediately stale. Caught by
`thumbnail_etag_content_keyed.hurl`, where two consecutive GETs of an
unchanged file stopped revalidating to 304.
The flip is what removes the hazard: once the derived tier is
authoritative it is populated before it is consulted, so there is no
window in which the row appears between two reads.
**The flip alone does NOT remove the hazard** *(corrected 2026-08-26 —
an earlier revision of this paragraph claimed it did)*. A first render
still creates the row as a side effect of producing the body, whatever
the read order, so two consecutive reads would still straddle its
appearance.
What actually removes it is resolving the ETag **after** generation on
the 200 path. A 304 can only fire when the client already holds a
validator, which means it has been served before, which means the row
exists — so the *conditional* path can safely consult the derived hash
up front, while the *generating* path computes it from bytes it now
holds. That is a handler restructure, not an ordering change, and it is
the actual prerequisite for the derived-hash ETag.
**The disk cache is `CachedBlobBackend`, reused unchanged.** No
thumbnail-specific cache, no second root path. Routing derived
@@ -1310,10 +1320,23 @@ hardcoded SQL). New sources bolt on independently.
**re-keyed** file→content on import (legitimate only because a
transcode is derivable). Its `.skip` markers — a cached negative
verdict with no bytes — remain an open question.
c. **Flip the read order**, derived first. The HTTP ETag's
source-keyed fallback becomes unreachable here; the attached and
derived halves already landed early, forced by the attachment
case (see *HTTP ETag*).
c. **Flip the read order**, derived first — **done 2026-08-26**. Two
things it is not: a two-line swap, and an ETag fix.
A derived miss must **fall through** to the sidecar, where the old
code terminated the lookup — while the imports drain, most content
has a sidecar and no row, so terminating would report "no
thumbnail" for nearly everything.
And it is **WebP-only**. `store_derived_blob` writes `image/webp`
with `variant` keyed on size alone, no format term, so a JPEG
request matches the WebP row and gets the wrong codec — a
regression the old ordering hid, because the `.jpg` sidecar won
first. JPEG clients therefore stay on the sidecar, **and the
sidecar cannot be deleted for them** until `variant` encodes
format. That is a new prerequisite for (e), not a detail: it means
a migration to `(kind, variant, format)` — or a format term inside
`variant` — has to land before the directories can go.
d. **Enable deletion** in the import jobs (opt-in, readback-verified).
e. **Remove the fallback read path** once the directory no longer
*exists* — not merely once it is empty. Two reasons. Empty is a