fix(compute): recover Error-phase sandboxes on gateway startup - #2269
Conversation
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.
|
@r3v5 This is great, thank you for looking into this and proposing a fix. Two points I was hoping to get some clarification on or possibly some changes.
|
Hi, @maxamillion ! Thanks for the review! I have addressed your two points in the new commit:
|
a705b0e to
48d84b4
Compare
|
Hey @elezar ! Could you please take a look on my PR? Thanks! |
48d84b4 to
0e27573
Compare
0e27573 to
5d6f952
Compare
|
Rebased PR from main and solved merge conflicts. |
5d6f952 to
bf0adaa
Compare
|
@pimlock @krishicks can I get some eyes? |
bf0adaa to
bd649a6
Compare
Sandboxes whose container exited on its own (e.g. SIGTERM from a Podman or Docker machine restart) were left stuck in the Error phase even though the container could be restarted. The startup sweep skipped every Error-phase sandbox unconditionally. Include Error-phase sandboxes in the startup sweep when their Ready condition reason marks a container exit or stop. If the driver restarts the container, move the sandbox back to Provisioning with a Resumed condition; if the container is gone or the start fails, leave the Error state untouched. Genuine (non-container-exit) errors are still skipped. Container-exit restart is handled by the drivers' existing idempotent start_sandbox path, which already restarts stopped/exited containers. The ContainerExited/ContainerStopped condition reasons are promoted to shared constants in openshell-core so the gateway and drivers agree on the recovery signal. Fixes NVIDIA#2179 Signed-off-by: Ian Miller <milleryan2003@gmail.com>
bd649a6 to
428b859
Compare
|
Hi @johntmyers ! Can you PTAL? |
johntmyers
left a comment
There was a problem hiding this comment.
gator-agent
PR Review Status
Thanks @r3v5. I checked your new runtime-restart reason, the recovery allowlist, and the regression coverage. GATOR-428b8592-01 is resolved for ordinary Podman exits, and I resolved its review thread. One related current-head blocker remains: Docker still records every exited container as ContainerExited, so the tightened allowlist can no longer recover Docker machine-restart victims.
Action required: classify Docker exit 137/143 records distinctly from ordinary exits and cover both recovery and terminal behavior.
Blocking findings:
GATOR-b58ecee3-01: Docker runtime-restart recovery is unreachable under the new reason gate.
Carried findings:
- None
Gator metadata
- Validation: Project-valid concentrated fix for linked issue #2179.
- Docs: No Fern update needed because this remains an internal startup-recovery correction without a CLI, API, configuration, or documented-workflow change.
- Checks: Current-head required checks are pending and test dispatch is deferred while review feedback remains.
- E2E:
test:e2eremains applied; current-head E2E has not yet been confirmed queued, running, or complete. - Head SHA:
b58ecee398d26a49816edb74852a74c90cddb8f0 - Base SHA:
de4c1fecf564cd16854447ec9c08659e7b64061b - Merge base SHA:
de4c1fecf564cd16854447ec9c08659e7b64061b - Patch ID:
a0286570c067ddc08898e7be0fd7a431c24653e7 - Gator payload:
7 - Review mode:
follow_up - Previous reviewed SHA:
428b85924b0cb2aad3dba4e3055b55a59e6ddfc3 - Review budget exhausted:
no - Maintainer decision required:
no - Next state:
gator:in-review
Mirror the Podman classification in the Docker driver so that externally signal-killed containers (exit 137/143, non-OOM) are persisted with the ContainerRuntimeRestart ready reason and become eligible for startup recovery, while ordinary exits and OOM kills stay terminal. current_snapshots now inspects EXITED containers to obtain the exit code and OOM flag, then applies the shared exit classification. Without the inspect step the list-summary path lacks an exit code, so the recovery gate could never fire for Docker. Adds unit tests covering signal-kill reclassification (137/143), ordinary exit staying terminal (exit 1), and OOM staying terminal despite exit 137. Signed-off-by: Ian Miller <milleryan2003@gmail.com>
|
Addressed GATOR-b58ecee3-01 (Docker restart recovery unreachable) in b778f01. The Docker driver now mirrors the Podman classification. Verify: an exited container with code 143 and New unit tests: |
|
/ok to test b778f01 |
johntmyers
left a comment
There was a problem hiding this comment.
gator-agent
PR Review Status
Thanks @r3v5. I checked the Docker exit inspection and classification you added at b778f01f, including the ordinary-exit and OOM safeguards and their regression coverage. GATOR-b58ecee3-01 is resolved, its review thread is closed, and no blocking findings remain.
Blocking findings:
- No blocking findings remain
Carried findings:
- None
Gator metadata
- Validation: Project-valid concentrated fix for linked issue #2179.
- Docs: No Fern update needed because this remains an internal startup-recovery correction without a CLI, API, configuration, or documented-workflow change.
- Checks: Current-head mirror setup is pending; required Branch Checks, Helm Lint, and E2E have not yet been confirmed queued, running, or complete.
- E2E:
test:e2eis applied and/ok to test b778f01f568f747923c8c1b4cf1c252e6d4d80e0was posted; mirror setup is pending before E2E dispatch can be confirmed. - Head SHA:
b778f01f568f747923c8c1b4cf1c252e6d4d80e0 - Base SHA:
de4c1fecf564cd16854447ec9c08659e7b64061b - Merge base SHA:
de4c1fecf564cd16854447ec9c08659e7b64061b - Patch ID:
23efc9c3b51e15bd31581e2e1173f3749ba6b9ed - Gator payload:
7 - Review mode:
follow_up - Previous reviewed SHA:
b58ecee398d26a49816edb74852a74c90cddb8f0 - Review budget exhausted:
no - Maintainer decision required:
no - Next state:
gator:in-review
Merge Decision NudgeThis PR has been in @NVIDIA/openshell-maintainers @NVIDIA/openshell-codeowners @mrunalp @sjenning @derekwaynecarr, can someone merge this PR or close/request changes if it should not proceed? |
Monitoring CompleteMonitoring is complete because this PR has merged. Final status: The PR reached I removed the active |



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