From e7feddeb6684cd6bb3288bfe5e6c20628194d7fe Mon Sep 17 00:00:00 2001 From: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Date: Wed, 14 Oct 2026 09:45:48 -0400 Subject: [PATCH] buzz-db: simplify FK-child purge test and document same-workflow attach Address review on #8317: the held-tx race scaffold could not fail without the admission barrier (the FK key-share row lock blocked quiescing anyway), so seed children in sequence and rename the test; the barrier stays pinned by begin_quiescing_waits_for_open_admitted_writer_note_update. Select the heartbeat by executor_id so a SET NULL regression fails the assertion, and document why attach_scheduled_workflow_run rejects cross-workflow runs. Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> --- crates/buzz-db/src/store/deletion.rs | 37 ++++++++----------- crates/buzz-db/src/store/workflow.rs | 4 ++ .../public/scheduled_workflow_fires.sql | 4 +- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/crates/buzz-db/src/store/deletion.rs b/crates/buzz-db/src/store/deletion.rs index 7993534160..3bf2b3924a 100644 --- a/crates/buzz-db/src/store/deletion.rs +++ b/crates/buzz-db/src/store/deletion.rs @@ -7741,11 +7741,12 @@ mod postgres_tests { } /// The whole-community purge is the only supported hard delete of channels, - /// personal-read accounts, and users. A child committed just before the - /// quiesce boundary must be included even with every FK action retired. + /// personal-read accounts, and users. With every FK action retired, each + /// child must still be deleted explicitly (the quiesce barrier itself is + /// pinned by `begin_quiescing_waits_for_open_admitted_writer_note_update`). #[tokio::test] #[ignore = "requires Postgres"] - async fn purge_removes_parent_children_after_concurrent_insert_without_fk_actions() { + async fn purge_removes_fk_children_explicitly_without_fk_actions() { let (db, store) = store().await; crate::test_support::retire_foreign_key_delete_actions(&db.pool).await; let (request, inventory) = inventoried_request(&db, &store).await; @@ -7796,11 +7797,12 @@ mod postgres_tests { .execute(&db.pool) .await .expect("seed operator feedback"); + let executor_id = format!("purge-test-{}", Uuid::new_v4()); sqlx::query( "INSERT INTO community_deletion_executor_heartbeats \ (executor_id, mode, request_id) VALUES ($1, 'worker', $2)", ) - .bind(format!("purge-test-{}", Uuid::new_v4())) + .bind(&executor_id) .bind(request.id) .execute(&db.pool) .await @@ -7818,7 +7820,7 @@ mod postgres_tests { let mut writer = db .begin_event_write_transaction(community) .await - .expect("admit child inserter"); + .expect("admit child seed"); sqlx::query( "INSERT INTO channel_members (community_id, channel_id, pubkey) VALUES ($1, $2, $3)", ) @@ -7827,7 +7829,7 @@ mod postgres_tests { .bind(&owner) .execute(writer.conn()) .await - .expect("insert member before quiescing"); + .expect("seed channel member"); sqlx::query( "INSERT INTO personal_read_frontiers \ (community_id, actor, channel_id, through_timestamp) VALUES ($1, $2, $3, now())", @@ -7837,21 +7839,12 @@ mod postgres_tests { .bind(channel) .execute(writer.conn()) .await - .expect("insert read frontier before quiescing"); - - let name = format!("purge-child-{}", Uuid::new_v4().simple()); - let quiesce_store = DeletionStore::new(crate::test_support::named_pool(&name).await); - let lease = claim.lease.clone(); - let quiescing = tokio::spawn(async move { quiesce_store.begin_quiescing(&lease).await }); - crate::test_support::wait_for_lock_wait(&db.pool, &name).await; - writer - .commit() + .expect("seed read frontier"); + writer.commit().await.expect("commit seeded children"); + store + .begin_quiescing(&claim.lease) .await - .expect("commit child insert before quiesce"); - quiescing - .await - .expect("join quiesce") - .expect("quiesce after child commit"); + .expect("quiesce"); let generation = store.fence(&claim.lease).await.expect("fence"); let token = LeaseToken { @@ -7892,9 +7885,9 @@ mod postgres_tests { ); let heartbeat_request: Option = sqlx::query_scalar( "SELECT request_id FROM community_deletion_executor_heartbeats \ - WHERE request_id = $1 LIMIT 1", + WHERE executor_id = $1", ) - .bind(request.id) + .bind(&executor_id) .fetch_one(&db.pool) .await .expect("heartbeat survives with retained request"); diff --git a/crates/buzz-db/src/store/workflow.rs b/crates/buzz-db/src/store/workflow.rs index a800a9eab2..6b927f668d 100644 --- a/crates/buzz-db/src/store/workflow.rs +++ b/crates/buzz-db/src/store/workflow.rs @@ -589,6 +589,10 @@ pub async fn latest_scheduled_workflow_fire( /// boundary. If run creation succeeds, callers should attach the run id before /// spawning execution. If run creation fails, leaving `workflow_run_id` NULL is /// intentional: the schedule instant was claimed and must not duplicate later. +/// +/// Returns `false` when the run is missing or belongs to another workflow. +/// Workflow delete removes only its own fires before its runs, and the fire→run +/// FK is NO ACTION, so a cross-workflow link would block that delete. pub async fn attach_scheduled_workflow_run( pool: &PgPool, community_id: CommunityId, diff --git a/schema/tables/public/scheduled_workflow_fires.sql b/schema/tables/public/scheduled_workflow_fires.sql index 507bde2d21..73be34a699 100644 --- a/schema/tables/public/scheduled_workflow_fires.sql +++ b/schema/tables/public/scheduled_workflow_fires.sql @@ -10,7 +10,9 @@ -- The FK to workflow_runs uses NO ACTION (not SET NULL): community_id is shared -- with the claim PK and is NOT NULL, so SET NULL is unimplementable here; a future -- delete of a still-linked run is blocked rather than orphaning the at-most-once --- claim row. workflow_runs are not pruned today, so this is a guardrail, not a path. +-- claim row. Workflow delete removes the workflow's own fires before its runs, so +-- attach_scheduled_workflow_run only links a run of the same workflow; a +-- cross-workflow link would otherwise block that delete. CREATE TABLE scheduled_workflow_fires ( community_id UUID NOT NULL REFERENCES communities(id),