docs(plan): job-driven sidecar deletion, and the persist-consolidation blocker

Two revisions from working through step 10.

**Deletion moves into the import jobs, not a release.** Sidecars are
local disk, so a release cannot know whether every instance has drained
— gating on "an empty tail" asks an operator to coordinate a fact
nothing reports, and there is no telling when or whether they trigger
the jobs at all. Each job unlinking what it has imported makes every
instance drain itself. Constrained three ways: verify the derived blob
reads back before unlinking (a store that reported success but landed
unreadable would otherwise take the last copy), only after the
read-order flip (or the derived tier takes its first production traffic
by accident), and opt-in, since a migration that deletes by default is
surprising. Scheduled tick rather than boot trigger — idempotent and
resumable, so periodic is safe, while walking .thumbnails/ at startup
delays readiness for nothing.

**Found while checking the dual-write assumption: it does not hold.**
store_derived_blob has ONE call site; fs::write(&thumb_path, …) has
five. get_thumbnail, generate_and_persist and
generate_all_sizes_background all persist sidecar-only. That breaks the
migration's premise rather than being untidy — on-demand renders keep
producing un-migrated state after the import runs, so the tail never
empties and the deletion gate never opens. One persist_thumbnail owning
sidecar + derived + moka is therefore a prerequisite, and it makes "stop
writing sidecars" a later one-line change instead of four edits. Noted
that ThumbnailService holds no DedupService, so it must be threaded
through.

Also corrects a claim I put in thumb_derived_import's own docs:
transcoding is NOT a later step. ImageTranscodeService exists and caches
.transcoded/{ext}/{file_id}.{ext}, so a third import is needed and it
must re-key file→content — legitimate only because a transcode is
derivable. Its .skip markers remain an open question.
This commit is contained in:
Edouard Vanbelle
2026-08-26 09:07:45 +02:00
parent 671e6ac0e7
commit 4ae1531286
2 changed files with 101 additions and 13 deletions
@@ -9,10 +9,12 @@
//! and records the mapping, after which the derived tier can become
//! authoritative and the sidecar can be deleted.
//!
//! **Thumbnails only, and that is permanent.** The table also holds
//! `kind = 'transcode'`, but transcoding lands *after* this migration, so
//! transcodes are born into the table and never pass through a sidecar era.
//! This job will not grow a transcode arm.
//! **Thumbnails only.** The table also holds `kind = 'transcode'`, and those
//! need their own import — `ImageTranscodeService` already exists and caches
//! to `.transcoded/{ext}/{file_id}.{ext}`, a different tree with a different
//! key. Importing them means **re-keying** file→content, which is legitimate
//! only because a transcode is derivable from the source bytes. Separate job;
//! this one will not grow a transcode arm.
//!
//! ### Idempotent by construction
//!