Uh oh!
There was an error while loading. Please reload this page.
fix(compute): recover Error-phase sandboxes on gateway startup - #2269
fix(compute): recover Error-phase sandboxes on gateway startup#2269r3v5 wants to merge 3 commits into
Conversation
594ab91 to
4150476Comparer3v5
commented
Jul 14, 2026
I have read the DCO document and I hereby sign the DCO. |
r3v5
commented
Jul 14, 2026
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:
pubasyncfnresume_sandbox(&self,sandbox_name:&str) -> Result<bool,ComputeDriverError>{let name = container::container_name(sandbox_name);let inspect = matchself.client.inspect_container(&name).await{Ok(i) => i,Err(PodmanApiError::NotFound(_)) => returnOk(false),Err(e) => returnErr(ComputeDriverError::from(e)),};if inspect.state.running{returnOk(true);}matchself.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.
@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.
|
| /// Container/pod label carrying the sandbox namespace. | ||
| pub const LABEL_SANDBOX_NAMESPACE: &str = "openshell.ai/sandbox-namespace"; | ||
| // --------------------------------------------------------------------------- |
There was a problem hiding this comment.
Made constants public and reusable.
| } | ||
| ContainerSummaryStateEnum::EXITED => { | ||
| ("False", "ContainerExited", "Container exited", false) | ||
| ("False", openshell_core::driver_utils::CONDITION_EXITED, "Container exited", false) |
r3v5
commented
Jul 16, 2026
Hi, @maxamillion ! Thanks for the review! I have addressed your two points in the new commit:
|
a705b0e to
48d84b4Comparer3v5
commented
Jul 23, 2026
Hey @elezar ! Could you please take a look on my PR? Thanks! |
48d84b4 to
0e27573Compare0e27573 to
5d6f952Comparer3v5
commented
Aug 20, 2026
Rebased PR from main and solved merge conflicts. |
5d6f952 to
bf0adaaComparer3v5
commented
Aug 20, 2026
@pimlock@krishicks can I get some eyes? |
bf0adaa to
bd649a6CompareSandboxes 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. FixesNVIDIA#2179 Signed-off-by: Ian Miller <milleryan2003@gmail.com>
bd649a6 to
428b859Comparer3v5
commented
Aug 24, 2026
Hi @johntmyers ! Can you PTAL? |
Label |
johntmyers
commented
Aug 24, 2026
/ok to test 428b859 |
johntmyers
left a comment
There was a problem hiding this comment.
gator-agent
PR Review Status
Thanks @maxamillion, I checked both concerns you raised. The recovery counter now advances only after the store transition succeeds, but the reason filter still groups ordinary application exits with machine/runtime restarts, so one blocking correctness issue remains.
Action required: distinguish runtime-restart recovery from generic container exits and add a regression test showing ordinary crashes remain terminal.
Blocking findings:
GATOR-428b8592-01: genericContainerExitederrors are restarted during gateway startup.
Carried findings:
- None
Non-blocking suggestions:
- None
Gator metadata
- Validation: Project-valid concentrated fix for linked issue #2179; duplicate search found no competing issue or PR.
- Docs: No Fern update needed because this corrects internal startup recovery without changing a CLI, API, configuration, or documented workflow.
- Checks: Current-head Branch E2E Checks are queued/running; review feedback takes precedence.
- E2E:
test:e2eapplied;/ok to test 428b85924b0cb2aad3dba4e3055b55a59e6ddfc3accepted and current-head workflow run 32768589180 is active. - Head SHA:
428b85924b0cb2aad3dba4e3055b55a59e6ddfc3 - Base SHA:
de4c1fecf564cd16854447ec9c08659e7b64061b - Merge base SHA:
de4c1fecf564cd16854447ec9c08659e7b64061b - Patch ID:
5451afaf080976211094feaf58264a271681a97b - Gator payload:
7 - Review mode:
initial - Previous reviewed SHA:
none - Review budget exhausted:
no - Maintainer decision required:
no - Next state:
gator:in-review
Uh oh!
There was an error while loading. Please reload this page.
…ecovery Startup recovery treated every non-OOM container exit as recoverable, lumping application crashes and non-zero exits together with machine/daemon-restart signal kills under the shared `ContainerExited` Ready-condition reason. On the next gateway startup it restarted those crashed workloads and replaced the terminal error with `Resumed`, erasing the failure signal and repeatedly reviving crash-prone sandboxes. Introduce a distinct `ContainerRuntimeRestart` reason for containers terminated by an external signal (exit 137/143 = SIGKILL/SIGTERM), which is the signature of a machine/daemon restart. The Podman inspect-based classification emits it; the startup recovery gate now recovers only `ContainerRuntimeRestart` and `ContainerStopped`. Ordinary `ContainerExited` records stay terminal, so genuine failures keep their error signal. Because only the new reason is recoverable and older gateways never persisted it, exits recorded before this change also stay terminal after an upgrade. Add regression coverage: a generic `ContainerExited` error is not relaunched, a signal-kill classifies as `ContainerRuntimeRestart`, and the startup sweep recovers only the runtime-restart record. Signed-off-by: Ian Miller <milleryan2003@gmail.com>
r3v5
commented
Aug 25, 2026
Addressed the blocking finding Root cause: startup recovery treated every non-OOM exit as recoverable because Podman classified both application exits and signal kills under the shared Fix: added a distinct Coverage added:
|
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
Uh oh!
There was an error while loading. Please reload this page.
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>
r3v5
commented
Aug 25, 2026
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: |
johntmyers
commented
Aug 25, 2026
/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



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