@@ -4434,18 +4434,23 @@ impl ComputeRuntime {
44344434 SandboxPhase::try_from(sandbox.phase()).unwrap_or(SandboxPhase::Unknown)
44354435 });
44364436
4437- if existing_phase != SandboxPhase::Starting {
4437+ let ready_container_exit = existing_phase == SandboxPhase::Ready
4438+ && (driver_snapshot_confirms_stopped(&incoming)
4439+ || driver_snapshot_reports_runtime_restart(&incoming));
4440+ if existing_phase != SandboxPhase::Starting && !ready_container_exit {
44384441 return self.apply_sandbox_update_locked(incoming, existing).await;
44394442 }
44404443
44414444 // Any snapshot can already be queued when StartSandbox moves the
44424445 // durable phase to Starting. In particular, an old-generation Ready
44434446 // event followed by its terminal event can otherwise promote and then
44444447 // stop the new generation before the replacement supervisor connects.
4448+ // The replacement may already be Ready when the old exit arrives, so
4449+ // terminal container snapshots need the same check after readiness.
44454450 // Release the global watch lock, wait for that lifecycle operation,
44464451 // and then reread both the driver and store before applying an
44474452 // authoritative observation. Taking the per-sandbox gate only for
4448- // this ambiguous phase avoids delaying unrelated watch events behind
4453+ // these ambiguous snapshots avoids delaying unrelated watch events behind
44494454 // slow lifecycle operations.
44504455 let existing_name = existing_sandbox.as_ref().map_or_else(
44514456 || incoming.name.clone(),
@@ -4475,7 +4480,7 @@ impl ComputeRuntime {
44754480 {
44764481 warn!(
44774482 sandbox_id = %incoming.id,
4478- "Could not validate driver snapshot during sandbox start ; retaining current sandbox state"
4483+ "Could not validate driver snapshot against current runtime ; retaining current sandbox state"
44794484 );
44804485 return Ok(());
44814486 }
@@ -11604,6 +11609,65 @@ mod tests {
1160411609 }
1160511610 }
1160611611
11612+ #[tokio::test]
11613+ async fn container_exit_after_replacement_ready_is_revalidated() {
11614+ for (reason, live_exited) in [
11615+ ("ContainerExited", false),
11616+ ("ContainerExited", true),
11617+ ("ContainerStopped", false),
11618+ ("ContainerStopped", true),
11619+ ("ContainerRuntimeRestart", false),
11620+ ("ContainerRuntimeRestart", true),
11621+ ] {
11622+ let driver = ControlledDriver::new();
11623+ let mut sandbox =
11624+ sandbox_record("sb-restarted", "sandbox-restarted", SandboxPhase::Starting);
11625+ sandbox.status.as_mut().unwrap().restart_count = 1;
11626+ sandbox.status.as_mut().unwrap().exit_code = Some(17);
11627+ sandbox.status.as_mut().unwrap().main_process_instance_id = "previous".to_string();
11628+ let mut exited = ready_driver_sandbox(sandbox.object_id(), sandbox.object_name());
11629+ exited.status = Some(make_driver_status(make_driver_condition(
11630+ reason,
11631+ "container exited",
11632+ )));
11633+ let live = if live_exited {
11634+ exited.clone()
11635+ } else {
11636+ ready_driver_sandbox(sandbox.object_id(), sandbox.object_name())
11637+ };
11638+ driver.set_get_outcome(ControlledGetOutcome::Sandbox(Box::new(live)));
11639+ let runtime = test_runtime(driver).await;
11640+ runtime.store.put_message(&sandbox).await.unwrap();
11641+ register_test_supervisor_session(&runtime, sandbox.object_id());
11642+ runtime
11643+ .supervisor_session_connected(sandbox.object_id(), "replacement")
11644+ .await
11645+ .unwrap();
11646+
11647+ runtime.apply_sandbox_update(exited).await.unwrap();
11648+
11649+ let stored = runtime
11650+ .store
11651+ .get_message::<Sandbox>(sandbox.object_id())
11652+ .await
11653+ .unwrap()
11654+ .unwrap();
11655+ assert_eq!(
11656+ stored.phase(),
11657+ if live_exited {
11658+ SandboxPhase::Error
11659+ } else {
11660+ SandboxPhase::Ready
11661+ } as i32,
11662+ "only a live {reason} may fail the replacement runtime"
11663+ );
11664+ let status = stored.status.unwrap();
11665+ assert_eq!(status.main_process_instance_id, "replacement");
11666+ assert_eq!(status.restart_count, 1);
11667+ assert_eq!(status.exit_code, None);
11668+ }
11669+ }
11670+
1160711671 #[tokio::test]
1160811672 async fn stale_ready_snapshot_queued_before_start_is_revalidated() {
1160911673 let driver = ControlledDriver::new();
@@ -13375,14 +13439,18 @@ mod tests {
1337513439
1337613440 #[tokio::test]
1337713441 async fn unexpected_term_runtime_restart_transitions_to_error() {
13378- let runtime = test_runtime(Arc::new(TestDriver::default())).await;
13442+ let driver = ControlledDriver::new();
13443+ let runtime = test_runtime(driver.clone()).await;
1337913444 let sandbox = sandbox_record("sb-term-exit", "sandbox-term-exit", SandboxPhase::Ready);
1338013445 runtime.store.put_message(&sandbox).await.unwrap();
1338113446 let mut runtime_restart = ready_driver_sandbox(sandbox.object_id(), sandbox.object_name());
1338213447 runtime_restart.status = Some(make_driver_status(make_driver_condition(
1338313448 "ContainerRuntimeRestart",
1338413449 "container exited with status 143",
1338513450 )));
13451+ driver.set_get_outcome(ControlledGetOutcome::Sandbox(Box::new(
13452+ runtime_restart.clone(),
13453+ )));
1338613454
1338713455 runtime.apply_sandbox_update(runtime_restart).await.unwrap();
1338813456
0 commit comments