mirror of
https://github.com/kvcache-ai/AgentENV.git
synced 2026-11-05 01:17:36 +00:00
[GH-ISSUE #372] bug(orchestrator): resume racing shutdown strands the sandbox in Resuming and its paused artifacts are destroyed on next startup #96
Labels
No labels
bug
documentation
enhancement
enhancement
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
kvcache-ai/AgentENV#96
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @tm-worker on GitHub (Oct 12, 2026).
Original GitHub issue: https://github.com/kvcache-ai/AgentENV/issues/372
Affected component
Orchestrator sandbox lifecycle (
src/orchestrator/service.rs) + paused-sandbox persistence (src/orchestrator/persistence/file_backed.rs)AgentENV version
main@4968423(2026-10-12) — re-verified on this revision; first analyzed on34cdc80(2026-10-09). The affected files (src/orchestrator/service.rs,src/api/impls/sandbox.rs,src/api/impls/volumes.rs,src/volume.rs) are byte-identical between the two revisions.Host environment
rust:bookwormcontainer on Docker Desktop for macOS (Apple Silicon VM); plain userspacecargo test -p agentenv --lib— no KVM/ublk/network-namespace needed by the reproduction.GatedMarkResumingPersister) that wraps the repo's ownRecordingPersisterand gates the firstmark_resumingon a oneshot. The test is reproduced verbatim below ("Reproduction test"). It runs inside the orchestrator test module (src/orchestrator/tests.rs, same crate, so the helpers it references are that module's existing test infrastructure) with an in-memory metadata store.Operation and configuration
Default configuration. The window is between a resume's persisted
Resumingmark and thelaunch_sandboxentry gate — realistically hit when a node drains (rolling redeploy) while an auto-resume (proxy lookup on a paused sandbox) or a client resume is in flight.Steps to reproduce
Deterministic interleaving (unit test, full code below):
Paused(store state Paused, persisted recordlifecycle=Paused, artifacts underartifacts/<id>/).resume_sandbox(id); it passes the resume entry gate (shutdown not yet requested), CASes the storePaused→Resuming, and is about to / just didmark_resuming(a persisted, fsync'd write).is_shutting_down = true) — before Task A reacheslaunch_sandbox.launch_sandbox, whose FIRST statement isensure_accepting_lifecycle_operations()→Err(ShuttingDown). It returns without any rollback.Resuming. Nothing ever advances it.The test parks the resume task inside a gated
mark_resumingso step 3 lands inside the window deterministically, and asserts the outcome.Expected behavior
The failed resume must roll back to
Paused— exactly like every other failure path inlaunch_sandboxdoes viarollback_failed_launch_metadata(which performs aResuming→PausedCAS pluspersister.rollback_resuming). Either the entry-gate rejection for a Resume plan should run that rollback, or theResumingmark/CAS should happen after the gate.Actual behavior
ShuttingDownrejection at thelaunch_sandboxentry gate is the ONLY failure path that skips the rollback: build failure, protect failure, start failure, the three shutdown checks inside launch, add failure, wait-ready failure and the terminal-CAS failure all restorePaused(service.rs:2632-2662) — the entry gate atservice.rs:2400-2401does not.Resuming. resume, delete, pause and keep-alive all wait onwait_for_transition(Resuming)for 60s (WAIT_TRANSITION_TIMEOUT) and fail. The drain itself also stalls: shutdown cleanup waits up to 3 rounds × 60s on the stranded sandbox and reports "failed to preserve all sandboxes".lifecycle=Resumingas invalid and callscleanup_invalid_record— it deletes the record ANDremove_dir_alls the wholeartifacts/<id>/directory (file_backed.rs:278-282, "discarding paused sandbox record left in resuming state"). The paused memory snapshot and rootfs writable-layer copy are destroyed — even though this VM never began resuming (the launch was rejected before build started).Logs and diagnostics
Timestamped tracing excerpt from the first run (
34cdc80, Linux container):Re-run on
4968423(same behavior, same assertions):The test asserts
store.state == Resumingafter the ShuttingDown error — i.e. the bug behavior. With a correct rollback it would assert Paused.Reproduction test
Append the following to
src/orchestrator/tests.rs(its helpers —setup,paused_resume_metadata,make_orchestrator_without_background_with_factory_and_persister,MockBackendFactory,InMemoryMetadataStore— are that module's existing testinfrastructure) and run:
Reproduction frequency
Deterministic in the unit test (gated persister double). In production it requires the shutdown flag to land inside the CAS/mark→launch window (the window contains a fsync'd RocksDB write — milliseconds; rolling redeploys with traffic make this plausible).
Regression information
Not a regression we can trace — the resume rollback machinery exists for every other failure stage; only the entry-gate path lacks it. Existing tests cover shutdown rejection for CREATE plans only (
tests.rs:4761asserts the store is empty after a rejected create); no test covered the resume plan until now.Additional context
Fix sketch (happy to open a PR):
launch_sandbox, whenensure_accepting_lifecycle_operations()fails for a Resume plan, run the sameResuming→PausedCAS +persister.rollback_resumingused byrollback_failed_launch_metadata; orresume_sandbox_innerBEFORE the CAS (there is already an entry gate at the top ofresume_sandbox_inner— the race window is between that gate and the launch gate), keeping rollback for the launch path.The secondary unrolled early-exit in the same function (missing
paused_state,service.rs:1741-1744, after CAS+mark) should get the same treatment.Pre-submission checklist
@mvanhorn commented on GitHub (Oct 20, 2026):
Resume commits the store and the persisted record from paused to resuming before launch checks whether the process is still accepting work. The shutdown rejection at the start of launch returns immediately and is the only failure there that does not restore the paused record.
Paused runtime state is now required before the paused-to-resuming update, so a missing snapshot returns an internal error and does not mark the record resuming.
https://github.com/kvcache-ai/AgentENV/pull/400