From 9a93bbcfbd00298f5f77f768568c7762b9917070 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sun, 20 Sep 2026 16:03:20 -0400 Subject: [PATCH] Wait for the managed run to settle before the prune scenario deletes it The stored view reports a Petri run ended as soon as its own run.finished record is folded, which is before the server stores the terminal lifecycle record and settles the managed run in its map. The delete precheck reads that map, so a delete sent as soon as the API reports the run ended can be refused as active instead of by the held lease. The scenario now waits for the managed run to settle, through a test-support accessor for its status, before it deletes. Co-Authored-By: Claude Fable 5.1 --- lib/apps/fabro-server/src/server.rs | 13 +++++++++++++ .../fabro-server/tests/it/scenario/petri.rs | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/lib/apps/fabro-server/src/server.rs b/lib/apps/fabro-server/src/server.rs index 929e26b57..05502d0f1 100644 --- a/lib/apps/fabro-server/src/server.rs +++ b/lib/apps/fabro-server/src/server.rs @@ -1201,6 +1201,19 @@ impl AppState { &self.petri_projector } + /// The status the server holds for a managed run, so a test can wait + /// for the run to settle in the server's own map (what the delete + /// precheck reads) and not only in the stored view, which can report + /// the run ended first. + #[cfg(any(test, feature = "test-support"))] + #[must_use] + pub fn test_managed_run_status(&self, run_id: &RunId) -> Option { + self.runs + .lock() + .ok() + .and_then(|runs| runs.get(run_id).map(|managed_run| managed_run.status)) + } + /// The pool the Petri view tables live in, so a test can read them. #[cfg(any(test, feature = "test-support"))] pub fn test_petri_view_pool(&self) -> DbPool { diff --git a/lib/apps/fabro-server/tests/it/scenario/petri.rs b/lib/apps/fabro-server/tests/it/scenario/petri.rs index 9ad651660..9f60488a3 100644 --- a/lib/apps/fabro-server/tests/it/scenario/petri.rs +++ b/lib/apps/fabro-server/tests/it/scenario/petri.rs @@ -802,6 +802,9 @@ async fn deleting_a_run_prunes_its_host_workspace_through_petri() { let store = state.test_petri_run_store(); let key = RunKey::new(run_id.clone()); wait_for_free_lease(store, &key).await; + // The view reports the run ended from Petri's own finish, before the + // server settles the managed run the delete precheck reads. + wait_for_managed_settle(&state, &run_id).await; // A live handle on the run, as its worker holds one, refuses the // delete: Petri will not prune under a lease someone holds. @@ -858,6 +861,21 @@ async fn deleting_a_run_prunes_its_host_workspace_through_petri() { } /// Wait until no owner holds the run's lease. +/// Wait until the server's own map holds the run as ended. +async fn wait_for_managed_settle(state: &AppState, run_id: &str) { + let run_id: RunId = run_id.parse().expect("a run id"); + for _ in 0..500 { + if state + .test_managed_run_status(&run_id) + .is_none_or(|status| status.is_terminal()) + { + return; + } + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + } + panic!("the managed run did not settle"); +} + async fn wait_for_free_lease(store: &SqliteRunStore, key: &RunKey) { for _ in 0..500 { if store.owner(key).await.expect("reads the lease").is_none() {