From dce541dc490660666ba88a8dae256e1ed23916cb Mon Sep 17 00:00:00 2001 From: archipelago Date: Thu, 9 Jul 2026 09:45:06 -0400 Subject: [PATCH] fix(state): pending-boot Restarting overlay must not mask a user stop Formal gate take-2 went 4/5: iteration 5's vaultwarden stop-wait saw 'restarting' for the full 120s window. A reconcile pass had queued every app into pending_boot_starts moments before the stop marker landed, and the scanner's overlay painted the deliberately-stopped app Restarting until the slow sequential pass reached it (legacy container, so no unit state to contradict it). Two filters: the scanner overlay skips user-stopped ids (the marker is written before the stop runs, so it's reliably present), and the reconciler no longer queues apps whose lifecycle op is in flight into the pending overlay (it skips them via the same probe anyway). Co-Authored-By: Claude Fable 5 --- .../src/container/prod_orchestrator.rs | 21 +++++++++++++------ core/archipelago/src/server.rs | 6 ++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/core/archipelago/src/container/prod_orchestrator.rs b/core/archipelago/src/container/prod_orchestrator.rs index df4e6b8c..cc49b047 100644 --- a/core/archipelago/src/container/prod_orchestrator.rs +++ b/core/archipelago/src/container/prod_orchestrator.rs @@ -1473,12 +1473,21 @@ impl ProdContainerOrchestrator { // pass so the scanner overlays queued-but-down apps as Restarting // instead of Stopped. Each app is deregistered as its turn finishes, // so a start that genuinely failed shows its real state again. - crate::crash_recovery::pending_boot_starts_add(manifests.iter().flat_map(|lm| { - [ - lm.manifest.app.id.clone(), - compute_container_name(&lm.manifest), - ] - })); + crate::crash_recovery::pending_boot_starts_add( + manifests + .iter() + // An app mid-lifecycle-op must not enter the pending overlay: + // the scanner would show it Restarting while its stop worker + // is deliberately taking it down (and this pass will skip it + // via the same probe below anyway). + .filter(|lm| !crate::app_ops::lifecycle_op_in_flight(&lm.manifest.app.id)) + .flat_map(|lm| { + [ + lm.manifest.app.id.clone(), + compute_container_name(&lm.manifest), + ] + }), + ); for lm in manifests { let app_id = lm.manifest.app.id.clone(); let container_name = compute_container_name(&lm.manifest); diff --git a/core/archipelago/src/server.rs b/core/archipelago/src/server.rs index a125f6cd..48eb94f0 100644 --- a/core/archipelago/src/server.rs +++ b/core/archipelago/src/server.rs @@ -1277,10 +1277,16 @@ async fn scan_and_update_packages( // below knows this Restarting is scanner-authored (resolve it as soon // as podman reports a settled state) and not owned by an RPC restart // task (whose transitional state must be preserved). + // …but never for a user-stopped app: the stop marker is written + // before the stop runs, and a reconcile pass that queued the app + // moments earlier must not repaint the user's deliberate Stopped as + // Restarting (gate iteration-5 vaultwarden stop-wait, 2026-07-09 — + // the overlay held 'restarting' past the 120s window). if matches!( pkg.state, crate::data_model::PackageState::Stopped | crate::data_model::PackageState::Exited ) && crate::crash_recovery::is_pending_boot_start(id) + && !user_stopped.contains(id) { pkg.state = crate::data_model::PackageState::Restarting; pkg.exit_code = None;