fix(state): installed apps are held, never dropped, while their container is gone
Quadlet renders --rm, so every stop DELETES the container: a scan during the stop->start window legitimately sees nothing where an installed app lives. The state layer treated that as the app not existing — after three absent scans it removed the entry entirely, so dashboard tiles vanished mid-restart, gate waits read 'absent' (grafana, at load ~2, ruling out contention), and under daemon-restart churn the FIRST scan could publish a completely empty My Apps map (observed twice on 2026-08-09). Two guards, both keyed to the durable installed-apps.json registry: - Eviction: an id the registry says is installed, and the user has not uninstalled, is held as Stopped (health/exit cleared) instead of being removed. The next scan that sees the container restores live state; desired-state recovery still recreates genuinely lost containers; a deliberate uninstall clears the registry first and still disappears. - Empty scans: the first-scan exemption no longer applies when the registry is non-empty — better to keep saying "scanning" than "empty". Verified: scan-merge/absence/registry test filters 34/34. Also carries the v1.7.127-alpha changelog (user-benefit entries for the Tor self-heal, trusted invites, the three-layer mempool fix, vanishing apps, the Bitcoin version pin, and the smaller UI fixes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
e898f138d1
commit
89b03c47bd
@@ -1768,8 +1768,19 @@ async fn scan_and_update_packages(
|
||||
.unwrap_or(false);
|
||||
let update_changed = update_available != current_data.server_info.status_info.updated;
|
||||
|
||||
// Empty scan result = podman failure or timeout, preserve existing state
|
||||
if packages.is_empty() && !first_scan {
|
||||
// The durable installed set is the truth the scan must never contradict:
|
||||
// quadlet renders --rm, so every stop DELETES the container and a scan mid
|
||||
// stop->start legitimately sees nothing where an installed app lives.
|
||||
let installed_registry = crate::crash_recovery::load_installed_apps(data_dir).await;
|
||||
let user_uninstalled = crate::crash_recovery::load_user_uninstalled(data_dir).await;
|
||||
|
||||
// Empty scan result = podman failure or timeout, preserve existing state.
|
||||
// The first scan is NOT exempt when the durable registry says apps exist:
|
||||
// the daemon restarts mid-churn (gate runs, OTAs), and publishing that
|
||||
// first empty scan blanked the whole My Apps map — the dashboard showed a
|
||||
// node with zero apps until the next scan (observed twice, 2026-08-09,
|
||||
// once at load ~2). Better to keep saying "scanning…" than to say "empty".
|
||||
if packages.is_empty() && (!first_scan || !installed_registry.is_empty()) {
|
||||
if tor_changed || update_changed {
|
||||
let mut data = current_data;
|
||||
data.server_info.tor_address = tor_addr.clone();
|
||||
@@ -1919,6 +1930,29 @@ async fn scan_and_update_packages(
|
||||
let count = absence_tracker.entry(id.clone()).or_insert(0);
|
||||
*count += 1;
|
||||
if *count >= CONTAINER_ABSENCE_THRESHOLD {
|
||||
// An app the durable registry says is installed (and the user
|
||||
// has not uninstalled) must NEVER be dropped from the map just
|
||||
// because its container is momentarily gone — with --rm that
|
||||
// is every restart's normal window. Dropping it here is what
|
||||
// made dashboard tiles vanish mid-restart and gate waits read
|
||||
// 'absent' (grafana, 2026-08-09, at load ~2). Hold it as
|
||||
// Stopped instead; the next scan that sees the container
|
||||
// restores the live state, and desired-state recovery still
|
||||
// recreates genuinely lost containers.
|
||||
if installed_registry.contains(&id) && !user_uninstalled.contains(&id) {
|
||||
if let Some(entry) = merged.get(&id) {
|
||||
if entry.state != crate::data_model::PackageState::Stopped {
|
||||
let mut held = entry.clone();
|
||||
held.state = crate::data_model::PackageState::Stopped;
|
||||
held.health = None;
|
||||
held.exit_code = None;
|
||||
merged.insert(id.clone(), held);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
absence_tracker.remove(&id);
|
||||
continue;
|
||||
}
|
||||
debug!(
|
||||
"Removing {} from state after {} consecutive absent scans",
|
||||
id, count
|
||||
|
||||
Reference in New Issue
Block a user