fix(upload): editing a caption after release now regenerates the keepsake
edit_upload updated the caption/hashtags and nothing else. A caption lives in the HTML viewer keepsake (the ZIP holds media only — export.rs), so after release the downloadable viewer kept showing the OLD caption forever while the live feed showed the new one. Editing stays allowed while locked/released — like comments and likes, the lock freezes new uploads only (USER_JOURNEYS §9.3) — so the fix is to regenerate, not forbid: the edit and an invalidate_and_arm(ViewerOnly) share one transaction (same atomicity as delete_upload), then start_regen after commit. ViewerOnly carries the finished ZIP forward untouched since the media didn't change; when the gallery isn't released, invalidate_and_arm returns None and this is a no-op. Mutation-verified: without it, an edit doesn't bump the epoch and the caption test fails. Also (test isolation): the compression worker now carries a generation counter that TRUNCATE bumps. A worker queued on the concurrency semaphore when a truncate wiped media would otherwise wake in the NEXT test, fail to find its file, and broadcast upload-error/upload-deleted into that test's SSE stream — corrupting any test asserting on toasts or feed. It now abandons itself when the generation moved. No-op in production (TRUNCATE is the only caller). Export workers were already inert across a truncate (epoch guard on a fresh-UUID event). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
@@ -15,6 +16,10 @@ pub struct CompressionWorker {
|
||||
pool: PgPool,
|
||||
media_path: PathBuf,
|
||||
sse_tx: broadcast::Sender<SseEvent>,
|
||||
/// Bumped whenever the underlying data is reset out from under in-flight work (only the e2e
|
||||
/// TRUNCATE does this today). A task captures the value at spawn and abandons itself if it has
|
||||
/// changed by the time it runs — see `process`.
|
||||
generation: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
impl CompressionWorker {
|
||||
@@ -24,14 +29,32 @@ impl CompressionWorker {
|
||||
pool,
|
||||
media_path,
|
||||
sse_tx,
|
||||
generation: Arc::new(AtomicU64::new(0)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Invalidate all in-flight and queued compression work. Called by the e2e TRUNCATE endpoint:
|
||||
/// truncating deletes the upload rows and wipes `media/`, so a worker that was queued on the
|
||||
/// semaphore when the wipe happened would otherwise wake in the NEXT test, fail to find its
|
||||
/// file, and broadcast `upload-error` / `upload-deleted` into that test's live SSE stream —
|
||||
/// corrupting any test that asserts on toasts or feed contents. Bumping the generation makes
|
||||
/// those stale tasks return silently instead. A no-op in production (never called there).
|
||||
pub fn bump_generation(&self) {
|
||||
self.generation.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
/// Spawn a background task to process an uploaded file.
|
||||
pub fn process(&self, upload_id: Uuid, original_path: String, mime_type: String) {
|
||||
let worker = self.clone();
|
||||
let born_at = worker.generation.load(Ordering::SeqCst);
|
||||
tokio::spawn(async move {
|
||||
let _permit = worker.semaphore.acquire().await;
|
||||
// The data this task was queued against may have been reset while it waited for a permit
|
||||
// (e2e TRUNCATE). If so, its file and row are gone; doing anything — including
|
||||
// broadcasting a failure — would leak into an unrelated test. Abandon quietly.
|
||||
if worker.generation.load(Ordering::SeqCst) != born_at {
|
||||
return;
|
||||
}
|
||||
match worker.do_process(upload_id, &original_path, &mime_type).await {
|
||||
Ok(_) => {
|
||||
tracing::info!("compression completed for upload {upload_id}");
|
||||
|
||||
Reference in New Issue
Block a user