fix(compute): recover Error-phase sandboxes on gateway startup#2269
fix(compute): recover Error-phase sandboxes on gateway startup#2269r3v5 wants to merge 2 commits into
Conversation
When a Podman machine restarts, sandbox containers exit with SIGTERM (code 143) but remain on disk. This method inspects the container state and calls start_container if the container exists but is not running, enabling the gateway to recover sandboxes without user intervention. Signed-off-by: Ian Miller <milleryan2003@gmail.com>
…A#2179) After a Podman/Docker machine restart, all sandbox containers exit with SIGTERM (code 143). The gateway marks them Error, and on next startup the resume sweep skipped Error-phase sandboxes entirely — leaving them stuck until the user deleted and recreated them. Now resume_persisted_sandboxes() attempts Error-phase sandboxes too: - Ok(true): container exists and restarted — clear error, set phase to Provisioning so the watch loop promotes to Ready - Ok(false): container gone — leave as Error (no overwrite) - Err: driver failure — leave as Error (no overwrite) Also wires StartupResume for PodmanComputeDriver so the resume sweep runs on Podman-backed gateways (previously Docker-only). Signed-off-by: Ian Miller <milleryan2003@gmail.com>
594ab91 to
4150476
Compare
|
I have read the DCO document and I hereby sign the DCO. |
|
recheck |
r3v5
left a comment
There was a problem hiding this comment.
PR Review — Claude Code (Opus 4.6)
Overview
After Podman/Docker machine restart, sandbox containers exit with SIGTERM but remain on disk. Gateway previously skipped Error-phase sandboxes during resume sweep, leaving them stuck. This PR makes the resume sweep attempt Error-phase sandboxes — restarting containers that still exist, leaving Error untouched if the container is gone or the driver fails. Also wires StartupResume for Podman (was Docker-only).
Key Design Decisions
-
Include Error in resume sweep, only skip Deleting (
crates/openshell-server/src/compute/mod.rs:787) — Replacessandbox_phase_should_be_running()filter with simplephase == Deletingcheck. Error-phase sandboxes now get a recovery attempt instead of being permanently stuck. -
Guard against overwriting existing error state (
mod.rs:810-828) — When an already-Error sandbox fails resume (Ok(false)orErr), the original error reason is preserved. OnlyOk(true)clears error state. Prevents losing diagnostic info. -
clear_sandbox_error()resets to Provisioning, not Ready (mod.rs:895-930) — Recovered sandboxes go toProvisioningwith Ready condition reason"Resumed". Watch loop then drives them to Ready naturally, same path as fresh sandboxes. -
Podman
resume_sandbox()uses inspect-then-start (crates/openshell-driver-podman/src/driver.rs:695-712) — Inspects container first; if running returnsOk(true)without restart.NotFoundmapped toOk(false)at both inspect and start points for race safety.
Notable Code
crates/openshell-driver-podman/src/driver.rs:695:
pub async fn resume_sandbox(&self, sandbox_name: &str) -> Result<bool, ComputeDriverError> {
let name = container::container_name(sandbox_name);
let inspect = match self.client.inspect_container(&name).await {
Ok(i) => i,
Err(PodmanApiError::NotFound(_)) => return Ok(false),
Err(e) => return Err(ComputeDriverError::from(e)),
};
if inspect.state.running {
return Ok(true);
}
match self.client.start_container(&name).await {
Ok(()) => Ok(true),
Err(PodmanApiError::NotFound(_)) => Ok(false),
Err(e) => Err(ComputeDriverError::from(e)),
}
}Potential Concerns
- TOCTOU between inspect and start — Container could be removed between
inspect_containerandstart_container. Handled by catchingNotFoundon start, so no actual bug, but worth noting the race is accounted for. update_message_caswith generation0inclear_sandbox_error()(mod.rs:904) — Passing0as CAS generation means no optimistic concurrency check. Safe here since this runs at startup before watchers spawn (per doc comment), but fragile if ever called later. Matches existingmark_sandbox_error()pattern though.
Verdict
Solid fix. Tests cover all three outcomes (container exists, gone, driver error). Manual E2E verification thorough. Clean removal of now-unnecessary sandbox_phase_should_be_running(). No blocking concerns.
| /// silently revived. `Unspecified` is included because it is the proto | ||
| /// default value; persisted rows with that value should be reconciled | ||
| /// from the live driver state rather than skipped forever. | ||
| fn sandbox_phase_should_be_running(phase: SandboxPhase) -> bool { |
There was a problem hiding this comment.
Unused anymore after the changes.



Summary
Errorand on next startup the resume sweep skipped Error-phase sandboxes entirely — leaving them stuck until the user deleted and recreated them, losing state.resume_persisted_sandboxes()now attempts Error-phase sandboxes: if the container still exists it is restarted and the sandbox transitions back toProvisioning→Ready; if the container is gone or the driver fails, the sandbox stays inErrorwithout overwriting the original error reason.resume_sandbox()to the Podman driver and wiresStartupResumeforPodmanComputeDriverso the resume sweep runs on Podman-backed gateways (previously Docker-only).Related Issue
Closes #2179
Changes
crates/openshell-driver-podman/src/driver.rs— Addedresume_sandbox()method that inspects container state and callsstart_containerif the container exists but is not running.crates/openshell-server/src/compute/mod.rs:resume_persisted_sandboxes()to only skipDeleting(notError)clear_sandbox_error()method (mirrorsmark_sandbox_error()) to reset phase toProvisioningand set Ready condition to"Resumed"Ok(false)andErrarms to avoid overwriting existing error state on already-Error sandboxesStartupResumeforPodmanComputeDriverand wired it intonew_podman()recoveredcounter to summary log line for observabilitysandbox_phase_should_be_running()functionTesting
Unit tests (3 new + 1 updated)
resume_persisted_sandboxes_recovers_error_phase_when_container_exists—Ok(true)→ phase becomesProvisioning, Ready condition reason ="Resumed"resume_persisted_sandboxes_leaves_error_when_container_missing—Ok(false)→ phase staysError, no overwriteresume_persisted_sandboxes_leaves_error_when_resume_fails—Err→ phase staysError, no overwriteresume_persisted_sandboxes_resumes_running_phasesto expect Error-phase sandbox in called_idsAll 1032 tests pass across
openshell-serverandopenshell-driver-podman. Clippy clean.Manual verification
Full end-to-end reproduction of issue #2179:
openshell sandbox create --name my-podman-sandbox --provider vertex-prodReadyand containerUp (healthy)podman machine stoppodman machine startExited (143)and sandbox stuck inError(before fix — screenshot 1)Resumed sandbox ... phase=Error recovered=trueandSandbox resume sweep complete resumed=1 recovered=1(screenshot 2)Readyand containerUp (healthy)(screenshot 2)openshell termTUI (screenshot 3)Screenshot 1 — Before fix: sandbox stuck in Error after Podman machine restart
Screenshot 2 — After fix: gateway restart recovers sandbox to Ready
Screenshot 3 — Sandbox usable in OpenShell TUI after recovery
Checklist
🤖 Generated with Claude Code