Skip to content

fix(orchestrator): restore pause state after allocation failure - #100

Open
morluto wants to merge 2 commits into
kvcache-ai:mainfrom
morluto:fix/pause-allocation-rollback
Open

fix(orchestrator): restore pause state after allocation failure#100
morluto wants to merge 2 commits into
kvcache-ai:mainfrom
morluto:fix/pause-allocation-rollback

Conversation

@morluto

@morluto morluto commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Restores a running sandbox when paused-artifact-root allocation fails before the backend pause begins.

Previously, pause_sandbox_inner detached the runtime handle and proxy route and then propagated an allocation error with ?. That skipped the rollback used by later pause failure paths, leaving metadata in Pausing while the live runtime was no longer routable.

The failure path now restores the runtime handle and proxy route, transitions metadata from Pausing back to Running, and releases the temporary PausedSandbox image ownership before returning the allocation error. If the metadata transition also fails, the returned internal error preserves both failures.

                         allocate artifact root
Running -> Pausing -> detach handle + route --------X
                                                     |
                                                     v
                  release paused refs <- Running <- restore route + handle
                                                     |
                                                     v
                                          return allocation error

Closes #99.

How was it tested?

  • Added pause_artifact_root_allocation_failure_restores_running_for_retry, which deterministically fails the first artifact-root allocation and verifies:
    • metadata returns to Running with no paused state;
    • the runtime handle and ready proxy route are restored;
    • temporary paused-image ownership is released;
    • no paused state is persisted for the failed attempt; and
    • a second pause succeeds and reaches Paused.
  • cargo test -p agentenv --lib orchestrator::service::tests::pause_artifact_root_allocation_failure_restores_running_for_retry -- --exact --nocapture
  • cargo fmt --all -- --check
  • git diff --check main...HEAD

Compatibility

No API, persistence format, or successful pause behavior changes. This only changes rollback after an artifact-root allocation error that occurs before SandboxBackend::pause is called.

Suggested review order

  1. Review the allocation error branch in src/orchestrator/service.rs.
  2. Review the deterministic failure and retry assertions in src/orchestrator/tests.rs.

@morluto
morluto force-pushed the fix/pause-allocation-rollback branch from cfb1114 to 08aaba5 Compare July 31, 2026 12:36
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

🔍 OpenCodeReview found 3 issue(s) in this PR.

  • ✅ Successfully posted inline: 3 comment(s)

Comment thread src/orchestrator/service.rs
Comment thread src/orchestrator/tests.rs
Comment thread src/orchestrator/service.rs Outdated
Comment thread src/orchestrator/tests.rs
Comment thread src/orchestrator/tests.rs Outdated
Comment thread src/orchestrator/service.rs Outdated
Comment thread src/orchestrator/tests.rs Outdated
Comment thread src/orchestrator/tests.rs Outdated
Comment thread src/orchestrator/service.rs
Comment thread src/orchestrator/service.rs Outdated
Comment thread src/orchestrator/tests.rs Outdated
Comment thread src/orchestrator/service.rs Outdated
Comment thread src/orchestrator/persistence/file_backed.rs Outdated
Comment thread src/orchestrator/persistence/mod.rs Outdated
Comment thread src/orchestrator/service.rs Outdated
Comment thread src/orchestrator/tests.rs Outdated
Comment thread src/orchestrator/tests.rs Outdated
@morluto
morluto force-pushed the fix/pause-allocation-rollback branch from fec07fa to 3bfe936 Compare July 31, 2026 19:13
@morluto
morluto force-pushed the fix/pause-allocation-rollback branch from 3bfe936 to a465e71 Compare July 31, 2026 19:14
Comment on lines +1065 to 1072
if let Err(error) = self
.persister
.delete_record_and_artifacts(&sandbox_id)
.await
{
warn!(error = ?error, "failed to clean up persisted state for missing sandbox handle");
}
return Err(OrchestratorError::SandboxNotFound(sandbox_id));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · medium]
This discards the persistence deletion failure after the metadata has already been removed, leaving no retry path. A stale paused record is loaded back into the metadata store by load_all on restart, so this can resurrect the sandbox that this branch just removed (and orphan artifacts in the meantime). Make failed deletion durable/retryable via reconciliation, or preserve enough metadata to retry cleanup rather than reporting only SandboxNotFound.

Comment on lines +1083 to +1093
self.sandboxes.write().await.insert(sandbox_id, handle);
self.restore_proxy_route(sandbox_id, removed_proxy_route)
.await;
let restore_result = self
.store
.update_state_if_state(
&sandbox_id,
SandboxState::Running,
&[SandboxState::Pausing],
)
.await;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · high]
The runtime and proxy route become active before the fallible metadata rollback. update_state_if_state returns an error on a backend failure or state conflict; in that case this branch returns with a routable runtime while metadata may remain Pausing. Concurrent pause/delete calls can then wait indefinitely for that transition, while data-plane traffic still reaches the sandbox. If publishing Running fails, undo the handle/route restoration and stop or otherwise reconcile the runtime before returning; add a failure-injection test for this path.

Comment thread src/orchestrator/tests.rs
Comment on lines +1828 to +1834
assert_eq!(
image_refs.unpinned(),
vec![
RuntimeImageOwner::StartingSandbox(sandbox_id),
RuntimeImageOwner::PausedSandbox(sandbox_id),
]
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[test · high]
This expectation legitimizes releasing the only durable hold on runtime_artifacts as soon as the sandbox is restored. A maintenance pass can snapshot the running set while the handle is detached, then continue after this unpin; its stale snapshot contains neither this sandbox nor a hold, so GC may delete artifacts still used by the restored runtime. Please change the rollback implementation and this assertion so protection is retained or atomically transferred to a running/runtime owner until maintenance is guaranteed to observe the restored handle, and add a test that interleaves maintenance with this rollback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pause allocation failures leave the sandbox detached in Pausing

1 participant