fix(update): concurrent apply reads as progress, not failure; idle-IO extraction
Demo images / Build & push demo images (push) Successful in 5m18s

"Another update operation is already running" surfaced as a scary
failure while the update was in fact applying fine (OptiPlex, v1.7.118
rollout). The apply path now joins the in-flight install — same
overlay, same wait-for-new-version polling — and a concurrent download
attempt shows a calm in-progress note (EN+ES strings added). The
backend's tarball extractions run under ionice -c3 nice -n10 so a
200MB update can't starve podman/status calls into multi-minute
timeouts on small disks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-07-29 10:38:11 -04:00
co-authored by Claude Fable 5
parent 00f1892bf8
commit 49ec294dea
5 changed files with 48 additions and 8 deletions
+1 -1
View File
@@ -104,7 +104,7 @@ dependencies = [
[[package]] [[package]]
name = "archipelago" name = "archipelago"
version = "1.7.117-alpha" version = "1.7.118-alpha"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"archipelago-container", "archipelago-container",
+24 -7
View File
@@ -1589,10 +1589,24 @@ pub async fn apply_update(data_dir: &Path) -> Result<()> {
if !mk.success() { if !mk.success() {
anyhow::bail!("mkdir {} failed", staging_new); anyhow::bail!("mkdir {} failed", staging_new);
} }
let extract = // Idle-priority IO: extracting a 200MB tarball at normal
host_sudo(&["tar", "-xzf", &src.to_string_lossy(), "-C", &staging_new]) // priority starved small nodes so badly during the v1.7.118
.await // rollout that podman/status calls timed out for minutes and
.with_context(|| format!("Failed to extract {}", name))?; // the update looked broken. The extraction can take as long
// as it likes — the node staying responsive matters more.
let extract = host_sudo(&[
"ionice",
"-c3",
"nice",
"-n10",
"tar",
"-xzf",
&src.to_string_lossy(),
"-C",
&staging_new,
])
.await
.with_context(|| format!("Failed to extract {}", name))?;
if !extract.success() { if !extract.success() {
let _ = host_sudo(&["rm", "-rf", &staging_new]).await; let _ = host_sudo(&["rm", "-rf", &staging_new]).await;
anyhow::bail!("tar extraction failed for {}", name); anyhow::bail!("tar extraction failed for {}", name);
@@ -1687,9 +1701,12 @@ pub async fn apply_update(data_dir: &Path) -> Result<()> {
anyhow::bail!("mkdir {} failed", staging_new); anyhow::bail!("mkdir {} failed", staging_new);
} }
let extract = host_sudo(&["tar", "-xzf", &archive, "-C", &staging_new]) // Idle-priority IO — same reasoning as the frontend tarball.
.await let extract = host_sudo(&[
.with_context(|| format!("Failed to extract {}", name))?; "ionice", "-c3", "nice", "-n10", "tar", "-xzf", &archive, "-C", &staging_new,
])
.await
.with_context(|| format!("Failed to extract {}", name))?;
if !extract.success() { if !extract.success() {
let _ = host_sudo(&["rm", "-rf", &staging_new]).await; let _ = host_sudo(&["rm", "-rf", &staging_new]).await;
anyhow::bail!("tar extraction failed for {}", name); anyhow::bail!("tar extraction failed for {}", name);
+2
View File
@@ -706,6 +706,8 @@
"downloadFailed": "Download failed. Please try again.", "downloadFailed": "Download failed. Please try again.",
"applySuccess": "Update applied. The service will restart momentarily.", "applySuccess": "Update applied. The service will restart momentarily.",
"applyFailed": "Failed to apply update. You can try again or rollback.", "applyFailed": "Failed to apply update. You can try again or rollback.",
"applyInProgress": "An update is already being installed — hang tight, the node will restart when it finishes.",
"downloadInProgress": "An update download is already in progress — progress will appear above.",
"rollbackSuccess": "Rolled back to previous version. Service will restart.", "rollbackSuccess": "Rolled back to previous version. Service will restart.",
"rollbackFailed": "Rollback failed.", "rollbackFailed": "Rollback failed.",
"pullAndRebuild": "Pull & Rebuild", "pullAndRebuild": "Pull & Rebuild",
+2
View File
@@ -693,6 +693,8 @@
"downloadFailed": "La descarga fall\u00f3. Intente de nuevo.", "downloadFailed": "La descarga fall\u00f3. Intente de nuevo.",
"applySuccess": "Actualizaci\u00f3n aplicada. El servicio se reiniciar\u00e1 en un momento.", "applySuccess": "Actualizaci\u00f3n aplicada. El servicio se reiniciar\u00e1 en un momento.",
"applyFailed": "Error al aplicar la actualizaci\u00f3n. Puede intentar de nuevo o revertir.", "applyFailed": "Error al aplicar la actualizaci\u00f3n. Puede intentar de nuevo o revertir.",
"applyInProgress": "Ya se est\u00e1 instalando una actualizaci\u00f3n \u2014 espere, el nodo se reiniciar\u00e1 al terminar.",
"downloadInProgress": "Ya hay una descarga de actualizaci\u00f3n en curso \u2014 el progreso aparecer\u00e1 arriba.",
"rollbackSuccess": "Se revirti\u00f3 a la versi\u00f3n anterior. El servicio se reiniciar\u00e1.", "rollbackSuccess": "Se revirti\u00f3 a la versi\u00f3n anterior. El servicio se reiniciar\u00e1.",
"rollbackFailed": "Error al revertir.", "rollbackFailed": "Error al revertir.",
"pullAndRebuild": "Pull y Recompilar", "pullAndRebuild": "Pull y Recompilar",
+19
View File
@@ -978,6 +978,11 @@ async function downloadUpdate() {
updateInProgress.value = false updateInProgress.value = false
await loadStatus() await loadStatus()
showStatus(t('systemUpdate.upToDateMessage')) showStatus(t('systemUpdate.upToDateMessage'))
} else if (/already running/i.test(msg)) {
// A download/apply is already in flight (auto-updater or another tab)
// that's progress, not failure. Re-sync state and say so calmly.
await loadStatus()
showStatus(t('systemUpdate.downloadInProgress'))
} else { } else {
// A failed download is NOT a staged update return the UI to the // A failed download is NOT a staged update return the UI to the
// Download button so the user can retry, instead of stranding them on // Download button so the user can retry, instead of stranding them on
@@ -1065,6 +1070,20 @@ async function applyUpdate() {
setTimeout(() => window.location.reload(), 3000) setTimeout(() => window.location.reload(), 3000)
} }
} catch (e) { } catch (e) {
// "Another update operation is already running" is NOT a failure an
// apply (or the auto-updater) is already doing the work. Presenting it
// as an error made a successfully-updating node look broken (OptiPlex,
// v1.7.118 rollout). Join the in-flight apply instead: same overlay,
// same wait-for-new-version polling.
if (/already running/i.test(errorMessage(e))) {
applying.value = false
if (target) {
startInstallOverlay(target)
} else {
showStatus(t('systemUpdate.applyInProgress'))
}
return
}
showStatus(t('systemUpdate.applyFailed'), true) showStatus(t('systemUpdate.applyFailed'), true)
if (import.meta.env.DEV) console.warn('Apply failed', e) if (import.meta.env.DEV) console.warn('Apply failed', e)
applying.value = false applying.value = false