feat(sandbox): add main restart policy (#2798)

* feat(sandbox): add main restart policy

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sandbox): harden policy-driven restarts

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sandbox): address restart review feedback

Signed-off-by: Drew Newberry <385+drew@users.noreply.github.com>

* fix(sandbox): port restart policy to current runtime

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* perf(sandbox): restart promptly after terminal delivery

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

---------

Signed-off-by: Drew Newberry <anewberry@nvidia.com>
Signed-off-by: Drew Newberry <385+drew@users.noreply.github.com>
This commit is contained in:
Drew Newberry
2026-09-29 02:18:30 +00:00
committed by GitHub
parent 1358941b81
commit acbac9cb79
36 changed files with 2968 additions and 898 deletions
+66
View File
@@ -18,6 +18,7 @@ use tokio::time::{Instant, sleep};
const SANDBOX_PRESENCE_TIMEOUT: Duration = Duration::from_secs(30);
const SANDBOX_LIST_POLL_INTERVAL: Duration = Duration::from_millis(500);
const SANDBOX_RESTART_TIMEOUT: Duration = Duration::from_secs(60);
fn normalize_output(output: &str) -> String {
let stripped = strip_ansi(output).replace('\r', "");
@@ -1266,6 +1267,71 @@ async fn canonical_main_exit_255_is_not_retried_as_transport_failure() {
#[tokio::test]
#[serial(sandbox_lifecycle)]
async fn on_failure_policy_replaces_runtime_and_preserves_workspace() {
const FIRST_MARKER: &str = "initial-main-ready";
const SCRIPT: &str = r#"
marker=/sandbox/.openshell-restart-e2e
if [ -e "$marker" ]; then
printf 'replacement-%s\n' "$(cat /proc/sys/kernel/random/uuid)" > /sandbox/replacement-run
printf 'replacement-main-ready\n'
sleep 300
else
touch "$marker"
printf 'initial-%s\n' "$(cat /proc/sys/kernel/random/uuid)" > /sandbox/initial-run
printf 'initial-main-ready\n'
sleep 2
exit 17
fi
"#;
let mut sandbox = SandboxGuard::create_keep_with_args(
&["--restart-policy", "on-failure"],
&["sh", "-lc", SCRIPT],
FIRST_MARKER,
)
.await
.expect("create sandbox with OnFailure restart policy");
let deadline = Instant::now() + SANDBOX_RESTART_TIMEOUT;
let mut observed_replacement_starting = false;
let final_details = loop {
let details = sandbox_details(&sandbox.name).await;
observed_replacement_starting |= details.contains("Phase: Starting");
if observed_replacement_starting
&& details.contains("Phase: Ready")
&& details.contains("Restart count: 1")
{
break details;
}
assert!(
Instant::now() < deadline,
"sandbox did not complete its policy-driven restart within \
{SANDBOX_RESTART_TIMEOUT:?}; last details:\n{details}"
);
sleep(Duration::from_millis(250)).await;
};
assert!(
final_details.contains("Restart policy: on-failure"),
"restart policy should remain visible after replacement:\n{final_details}"
);
let runs = sandbox
.exec(&["cat", "/sandbox/initial-run", "/sandbox/replacement-run"])
.await
.expect("replacement sandbox should retain the first run's workspace");
assert!(
runs.contains("initial-"),
"missing initial run marker:\n{runs}"
);
assert!(
runs.contains("replacement-"),
"missing replacement run marker:\n{runs}"
);
sandbox.cleanup().await;
}
#[tokio::test]
async fn sandbox_create_with_no_keep_cleans_up_after_tty_command() {
let name = format!("tty-{:015x}", rand::random::<u64>() & 0x0fff_ffff_ffff_ffff);
// Capture startup diagnostics before --no-keep removes a failed container.