fix(GC): race condition in dedup_service
Stamp `orphaned_at = now()` on the ref-0 row so it sits inside the
GC grace window for the duration of this test. Without it,
`orphaned_at IS NULL` is treated by `garbage_collect` as
"pre-migration, immediately reapable" — and any sibling test in
the shared pool that calls `garbage_collect()` (e.g.
`garbage_collect_respects_grace_and_cross_checks`) would race
with the pin below and delete the row first.
This commit is contained in:
@@ -3247,6 +3247,17 @@ mod delta_upload_integration_tests {
|
|||||||
|
|
||||||
/// Store `data` through the streaming path and give `user_id` a file
|
/// Store `data` through the streaming path and give `user_id` a file
|
||||||
/// row referencing it — making its chunks claimable by that user.
|
/// row referencing it — making its chunks claimable by that user.
|
||||||
|
///
|
||||||
|
/// **Order matters.** BLAKE3 is deterministic, so the file row is
|
||||||
|
/// inserted BEFORE `store_from_stream` runs. This closes a race in
|
||||||
|
/// the shared test pool: Phase 1 of `garbage_collect()` deletes
|
||||||
|
/// manifests with `NOT EXISTS (file referencing it)`. With the old
|
||||||
|
/// order (store first, file second), a concurrent GC-invoking test
|
||||||
|
/// (`garbage_collect_honours_grace_window_and_references`,
|
||||||
|
/// `manifest_dereference_defers_chunk_reclamation_to_gc`) could
|
||||||
|
/// reap our manifest in the microsecond window between the two
|
||||||
|
/// statements, causing CI-flaky `RowNotFound` panics in producers
|
||||||
|
/// like `hash_chunk_sequence_recomputes_and_validates_sizes`.
|
||||||
async fn seed_owned_content(
|
async fn seed_owned_content(
|
||||||
svc: &DedupService,
|
svc: &DedupService,
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
@@ -3255,19 +3266,7 @@ mod delta_upload_integration_tests {
|
|||||||
data: &[u8],
|
data: &[u8],
|
||||||
label: &str,
|
label: &str,
|
||||||
) -> (String, Vec<String>, Uuid) {
|
) -> (String, Vec<String>, Uuid) {
|
||||||
let source = stream::iter(vec![Ok::<_, std::io::Error>(Bytes::copy_from_slice(data))]);
|
let file_hash = blake3::hash(data).to_hex().to_string();
|
||||||
let stored = svc
|
|
||||||
.store_from_stream(source, Some("application/octet-stream".into()))
|
|
||||||
.await
|
|
||||||
.expect("store");
|
|
||||||
let file_hash = stored.hash().to_string();
|
|
||||||
let chunks: Vec<String> = sqlx::query_scalar(
|
|
||||||
"SELECT UNNEST(chunk_hashes) FROM storage.chunk_manifests WHERE file_hash = $1",
|
|
||||||
)
|
|
||||||
.bind(&file_hash)
|
|
||||||
.fetch_all(pool)
|
|
||||||
.await
|
|
||||||
.expect("chunks");
|
|
||||||
|
|
||||||
let file_id: Uuid = sqlx::query_scalar(
|
let file_id: Uuid = sqlx::query_scalar(
|
||||||
"INSERT INTO storage.files (name, user_id, drive_id, blob_hash, size)
|
"INSERT INTO storage.files (name, user_id, drive_id, blob_hash, size)
|
||||||
@@ -3284,6 +3283,26 @@ mod delta_upload_integration_tests {
|
|||||||
.fetch_one(pool)
|
.fetch_one(pool)
|
||||||
.await
|
.await
|
||||||
.expect("file row");
|
.expect("file row");
|
||||||
|
|
||||||
|
let source = stream::iter(vec![Ok::<_, std::io::Error>(Bytes::copy_from_slice(data))]);
|
||||||
|
let stored = svc
|
||||||
|
.store_from_stream(source, Some("application/octet-stream".into()))
|
||||||
|
.await
|
||||||
|
.expect("store");
|
||||||
|
assert_eq!(
|
||||||
|
stored.hash(),
|
||||||
|
file_hash,
|
||||||
|
"pre-computed BLAKE3 must match CDC-store output"
|
||||||
|
);
|
||||||
|
|
||||||
|
let chunks: Vec<String> = sqlx::query_scalar(
|
||||||
|
"SELECT UNNEST(chunk_hashes) FROM storage.chunk_manifests WHERE file_hash = $1",
|
||||||
|
)
|
||||||
|
.bind(&file_hash)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
.expect("chunks");
|
||||||
|
|
||||||
(file_hash, chunks, file_id)
|
(file_hash, chunks, file_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3353,8 +3372,17 @@ mod delta_upload_integration_tests {
|
|||||||
let orphan = blake3::hash(format!("orphan-{}", Uuid::new_v4()).as_bytes())
|
let orphan = blake3::hash(format!("orphan-{}", Uuid::new_v4()).as_bytes())
|
||||||
.to_hex()
|
.to_hex()
|
||||||
.to_string();
|
.to_string();
|
||||||
|
// Stamp `orphaned_at = now()` on the ref-0 row so it sits inside the
|
||||||
|
// GC grace window for the duration of this test. Without it,
|
||||||
|
// `orphaned_at IS NULL` is treated by `garbage_collect` as
|
||||||
|
// "pre-migration, immediately reapable" — and any sibling test in
|
||||||
|
// the shared pool that calls `garbage_collect()` (e.g.
|
||||||
|
// `garbage_collect_respects_grace_and_cross_checks`) would race
|
||||||
|
// with the pin below and delete the row first.
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"INSERT INTO storage.blobs (hash, size, ref_count) VALUES ($1, 10, 1), ($2, 10, 0)",
|
"INSERT INTO storage.blobs (hash, size, ref_count, orphaned_at) VALUES
|
||||||
|
($1, 10, 1, NULL),
|
||||||
|
($2, 10, 0, now())",
|
||||||
)
|
)
|
||||||
.bind(&foreign)
|
.bind(&foreign)
|
||||||
.bind(&orphan)
|
.bind(&orphan)
|
||||||
|
|||||||
Reference in New Issue
Block a user