From adc3c444cdb2ef2a4e38c1442fc5c6e296091b7f Mon Sep 17 00:00:00 2001 From: archipelago Date: Sat, 8 Aug 2026 07:44:05 -0400 Subject: [PATCH] fix(container): an absent container is not proof of uninstallation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit installed_app_ids judged installation on live containers alone. Watched on archi-dev-box within the hour: lnd read as ABSENT, then as EXISTS again. Containers on this node come and go — the boot reconciler logs "previously-running app has no container after boot — recreating" for bitcoin-knots and electrumx repeatedly — so a momentary gap looked exactly like a removal, and the reaper would have taken a healthy companion's unit with it. ORPHAN_GRACE narrows that window but cannot close it: nothing bounds how long a gap lasts. An app now counts as installed if its container exists in any state OR its container name is in the durable last-running snapshot. That snapshot is what crash_recovery itself calls "installation evidence" and what reconcile_all_with_mode already trusts to recreate a previously-running app whose container vanished — the same signal, for the same reason, now shared rather than reinvented. Only fedimint is a true orphan on this box: it appears in no adoption list and has no quadlet unit of its own. lnd is installed and merely flapping, which is a separate bug. Container suite 215/215. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/container/prod_orchestrator.rs | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/core/archipelago/src/container/prod_orchestrator.rs b/core/archipelago/src/container/prod_orchestrator.rs index f66fb1a9..4a39c0b5 100644 --- a/core/archipelago/src/container/prod_orchestrator.rs +++ b/core/archipelago/src/container/prod_orchestrator.rs @@ -1686,14 +1686,27 @@ impl ProdContainerOrchestrator { /// from `Some(vec![])`: a caller that removes things on absence must not /// treat "I could not look" as "nothing is installed". /// - /// The presence of a container — in ANY state — is the whole test. It - /// deliberately does NOT carry over the `user_stopped` / `disabled` - /// filters the old `manifest_ids` applied: a stopped app is still an - /// installed app, its container still - /// exists (exited), and treating it as uninstalled would make stopping an - /// app tear its companion down and rebuild it on the next start. "Is it - /// installed" and "is it currently meant to be running" are different - /// questions, and only the first one belongs here. + /// An app counts as installed if its container exists in ANY state, OR its + /// container name appears in the durable last-running snapshot. + /// + /// Both halves are load-bearing. The snapshot is what `crash_recovery` + /// itself calls "installation evidence" and what `reconcile_all_with_mode` + /// already trusts to recreate a previously-running app whose container + /// vanished. It is needed here because a container's absence is NOT proof + /// of uninstallation: containers on this node demonstrably come and go — + /// `lnd` read as absent and then present again within the hour, and the + /// boot reconciler logs "previously-running app has no container after + /// boot — recreating" for bitcoin-knots and electrumx repeatedly. Judging + /// on live containers alone would let a momentary gap look like a removal + /// and cost a healthy companion its unit. + /// + /// This also deliberately does NOT carry over the `user_stopped` / + /// `disabled` filters the old `manifest_ids` applied: a stopped app is + /// still an installed app, its container still exists (exited), and + /// treating it as uninstalled would make stopping an app tear its + /// companion down and rebuild it on the next start. "Is it installed" and + /// "is it currently meant to be running" are different questions, and only + /// the first one belongs here. pub async fn installed_app_ids(&self) -> Option> { let present: std::collections::HashSet = self .runtime @@ -1703,12 +1716,16 @@ impl ProdContainerOrchestrator { .into_iter() .map(|c| c.name) .collect(); + let was_running = crate::crash_recovery::load_last_running_names(&self.data_dir).await; let state = self.state.read().await; Some( state .manifests .iter() - .filter(|(_, lm)| present.contains(&compute_container_name(&lm.manifest))) + .filter(|(_, lm)| { + let container = compute_container_name(&lm.manifest); + present.contains(&container) || was_running.contains(&container) + }) .map(|(app_id, _)| app_id.clone()) .collect(), )