Skip to content

Commit 262be8a

Browse files
committed
fix(config): sync latest supervisor readiness contract
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
2 parents 19a05f3 + c10f372 commit 262be8a

9 files changed

Lines changed: 1097 additions & 801 deletions

File tree

architecture/gateway.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -411,8 +411,8 @@ public descriptor set generated by `openshell-core`; a fingerprint test in
411411
Compute-driver, credential-driver, gateway-interceptor, and
412412
supervisor-middleware services are compiled contracts for internal extension
413413
boundaries, not public gateway RPCs. The current public inventory has 79
414-
methods, 316 messages, and 26 enums
415-
(`2293d2c57a4698fc2b1a7e9bd3470a97931fbbcf7be07c1284bb98a56255454f`).
414+
methods, 317 messages, and 26 enums
415+
(`284412343e72b26b7252e727dd66f2cc530ab5b317eb1ada0b1c3e6fcb80082e`).
416416
`ReportEndpointStatus` is a sandbox-authenticated public gateway RPC. Its request, response, and `EndpointObservation` messages belong only to the public closure. `EndpointStatus` and `EndpointResult` also belong to the durable closure because `Sandbox.status.endpoint_statuses` persists them. The repeated status field uses a new wire tag; stored sandboxes without it decode with an empty endpoint list and retain their lifecycle fields. A fixed payload encoded with the earlier sandbox schema verifies that no database rewrite is required.
417417

418418
Allow and deny append requests carry `L7RuleTarget` to declare the rule, endpoint, and complete affected scope. The removed `host` and `port` fields remain reserved by number and name, and requests without a target are rejected. These mutation requests are not persisted formats.

architecture/sandbox.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -662,12 +662,16 @@ sequenceDiagram
662662
GW->>SUP: ConfigurationAdmission(durable state)
663663
alt Configuration accepted
664664
SUP->>SUP: Launch workload
665+
SUP->>GW: SupervisorRuntimeReady
666+
GW->>DB: Promote sandbox to Ready
665667
else Configuration rejected
666668
GW->>SUP: ConfigUpdate(repaired snapshot)
667669
SUP->>GW: ConfigUpdateResult(component outcome, admission)
668670
GW->>DB: Persist accepted repair
669671
GW->>SUP: ConfigurationAdmission(accepted)
670672
SUP->>SUP: Launch workload on the same supervisor
673+
SUP->>GW: SupervisorRuntimeReady
674+
GW->>DB: Promote sandbox to Ready
671675
end
672676
```
673677

@@ -697,7 +701,10 @@ The gateway validates generation identity, persists admission, and returns that
697701
durable state on the stream. The supervisor holds the workload boundary until
698702
it receives an accepted acknowledgement. Rejection leaves the stream and
699703
supervisor alive so a later complete replacement can repair the generation and
700-
release the same workload. Both peers require protocol revision 3; revisions that rely on polling are rejected.
704+
release the same workload. After launch, the supervisor separately reports
705+
runtime readiness once its relay plane is usable; admission alone never promotes
706+
the sandbox to `Ready`. Both peers require protocol revision 3; revisions that
707+
rely on polling are rejected.
701708

702709
The gateway serializes construction per sandbox and component, and coalesces
703710
repeated mutations into the latest full snapshot. An enqueue result means only

crates/openshell-server/src/compute/mod.rs

Lines changed: 124 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,25 @@ impl fmt::Debug for ComputeRuntime {
654654
}
655655
}
656656

657+
#[derive(Clone, Copy)]
658+
enum SupervisorSessionStateUpdate<'a> {
659+
Connected {
660+
instance_id: &'a str,
661+
admission: Option<&'a openshell_core::proto::SandboxConfigurationAdmission>,
662+
readiness: SupervisorRuntimeReadiness,
663+
},
664+
Disconnected {
665+
terminal_delivery_finalized: bool,
666+
},
667+
}
668+
669+
#[derive(Clone, Copy)]
670+
enum SupervisorRuntimeReadiness {
671+
Initializing,
672+
Ready,
673+
ReadyAfterAdmission,
674+
}
675+
657676
impl ComputeRuntime {
658677
#[allow(clippy::too_many_arguments)]
659678
#[tracing::instrument(
@@ -1610,7 +1629,7 @@ impl ComputeRuntime {
16101629
) -> Option<Sandbox> {
16111630
let sandbox_id = transition.object_id().to_string();
16121631
let expected_resource_version = sandbox_resource_version(transition);
1613-
let session_connected = self.supervisor_sessions.has_session(&sandbox_id);
1632+
let session_connected = self.supervisor_sessions.is_runtime_ready(&sandbox_id);
16141633
match self
16151634
.store
16161635
.update_message_cas::<Sandbox, _>(&sandbox_id, expected_resource_version, |sandbox| {
@@ -2100,7 +2119,7 @@ impl ComputeRuntime {
21002119

21012120
match observed {
21022121
Ok(Some(snapshot)) if snapshot.id == sandbox_id && snapshot.status.is_some() => {
2103-
let session_connected = self.supervisor_sessions.has_session(sandbox_id);
2122+
let session_connected = self.supervisor_sessions.is_runtime_ready(sandbox_id);
21042123
self.write_delete_recovery_with_retry(
21052124
sandbox_id,
21062125
deleting_resource_version,
@@ -3312,7 +3331,7 @@ impl ComputeRuntime {
33123331
expected_resource_version: u64,
33133332
existing_phase: SandboxPhase,
33143333
) -> Result<(), String> {
3315-
let session_connected = self.supervisor_sessions.has_session(&incoming.id);
3334+
let session_connected = self.supervisor_sessions.is_runtime_ready(&incoming.id);
33163335
let sandbox = self
33173336
.store
33183337
.update_message_cas::<Sandbox, _>(
@@ -3354,8 +3373,31 @@ impl ComputeRuntime {
33543373
sandbox_id: &str,
33553374
instance_id: &str,
33563375
) -> Result<(), String> {
3357-
self.set_supervisor_session_state(sandbox_id, true, Some(instance_id), None, false)
3358-
.await
3376+
self.set_supervisor_session_state(
3377+
sandbox_id,
3378+
SupervisorSessionStateUpdate::Connected {
3379+
instance_id,
3380+
admission: None,
3381+
readiness: SupervisorRuntimeReadiness::Ready,
3382+
},
3383+
)
3384+
.await
3385+
}
3386+
3387+
pub async fn supervisor_runtime_ready(
3388+
&self,
3389+
sandbox_id: &str,
3390+
instance_id: &str,
3391+
) -> Result<(), String> {
3392+
self.set_supervisor_session_state(
3393+
sandbox_id,
3394+
SupervisorSessionStateUpdate::Connected {
3395+
instance_id,
3396+
admission: None,
3397+
readiness: SupervisorRuntimeReadiness::ReadyAfterAdmission,
3398+
},
3399+
)
3400+
.await
33593401
}
33603402

33613403
pub async fn supervisor_session_admission(
@@ -3366,10 +3408,15 @@ impl ComputeRuntime {
33663408
) -> Result<(), String> {
33673409
self.set_supervisor_session_state(
33683410
sandbox_id,
3369-
true,
3370-
Some(instance_id),
3371-
Some(admission),
3372-
false,
3411+
SupervisorSessionStateUpdate::Connected {
3412+
instance_id,
3413+
admission: Some(admission),
3414+
readiness: if self.supervisor_sessions.is_runtime_ready(sandbox_id) {
3415+
SupervisorRuntimeReadiness::Ready
3416+
} else {
3417+
SupervisorRuntimeReadiness::Initializing
3418+
},
3419+
},
33733420
)
33743421
.await
33753422
}
@@ -3381,54 +3428,72 @@ impl ComputeRuntime {
33813428
) -> Result<(), String> {
33823429
self.set_supervisor_session_state(
33833430
sandbox_id,
3384-
false,
3385-
None,
3386-
None,
3387-
terminal_delivery_finalized,
3431+
SupervisorSessionStateUpdate::Disconnected {
3432+
terminal_delivery_finalized,
3433+
},
33883434
)
33893435
.await
33903436
}
33913437

33923438
async fn set_supervisor_session_state(
33933439
&self,
33943440
sandbox_id: &str,
3395-
connected: bool,
3396-
instance_id: Option<&str>,
3397-
admission: Option<&openshell_core::proto::SandboxConfigurationAdmission>,
3398-
terminal_delivery_finalized: bool,
3441+
update: SupervisorSessionStateUpdate<'_>,
33993442
) -> Result<(), String> {
34003443
let _guard = self.sync_lock.lock().await;
34013444
let existing = self
34023445
.store
34033446
.get_message::<Sandbox>(sandbox_id)
34043447
.await
34053448
.map_err(|err| err.to_string())?;
3406-
self.set_supervisor_session_state_from_snapshot(
3407-
sandbox_id,
3408-
connected,
3409-
instance_id,
3410-
admission,
3411-
terminal_delivery_finalized,
3412-
existing,
3413-
)
3414-
.await
3449+
self.set_supervisor_session_state_from_snapshot(sandbox_id, update, existing)
3450+
.await
34153451
}
34163452

34173453
async fn set_supervisor_session_state_from_snapshot(
34183454
&self,
34193455
sandbox_id: &str,
3420-
connected: bool,
3421-
instance_id: Option<&str>,
3422-
admission: Option<&openshell_core::proto::SandboxConfigurationAdmission>,
3423-
terminal_delivery_finalized: bool,
3456+
update: SupervisorSessionStateUpdate<'_>,
34243457
mut existing: Option<Sandbox>,
34253458
) -> Result<(), String> {
3459+
let (connected, instance_id, admission, terminal_delivery_finalized, readiness) =
3460+
match update {
3461+
SupervisorSessionStateUpdate::Connected {
3462+
instance_id,
3463+
admission,
3464+
readiness,
3465+
} => (true, Some(instance_id), admission, false, Some(readiness)),
3466+
SupervisorSessionStateUpdate::Disconnected {
3467+
terminal_delivery_finalized,
3468+
} => (false, None, None, terminal_delivery_finalized, None),
3469+
};
3470+
let runtime_ready = matches!(
3471+
readiness,
3472+
Some(
3473+
SupervisorRuntimeReadiness::Ready | SupervisorRuntimeReadiness::ReadyAfterAdmission
3474+
)
3475+
);
3476+
let require_activated_configuration = matches!(
3477+
readiness,
3478+
Some(SupervisorRuntimeReadiness::ReadyAfterAdmission)
3479+
);
34263480
for attempt in 1..=SUPERVISOR_SESSION_CAS_RETRY_LIMIT {
34273481
let Some(current) = existing else {
34283482
return Ok(());
34293483
};
34303484
let current_phase =
34313485
SandboxPhase::try_from(current.phase()).unwrap_or(SandboxPhase::Unknown);
3486+
if connected
3487+
&& runtime_ready
3488+
&& require_activated_configuration
3489+
&& current
3490+
.status
3491+
.as_ref()
3492+
.and_then(|status| status.configuration_activated)
3493+
!= Some(true)
3494+
{
3495+
return Err("sandbox configuration is not durably admitted".to_string());
3496+
}
34323497
if connected
34333498
&& (provisioning_deadline::timed_out(&current)
34343499
|| matches!(
@@ -3468,7 +3533,6 @@ impl ComputeRuntime {
34683533
expected_resource_version,
34693534
|sandbox| {
34703535
if connected {
3471-
ensure_supervisor_ready_status(&mut sandbox.status);
34723536
let status = sandbox.status.get_or_insert_with(Default::default);
34733537
status.main_process_instance_id =
34743538
instance_id.unwrap_or_default().to_string();
@@ -3480,7 +3544,10 @@ impl ComputeRuntime {
34803544
== i32::from(openshell_core::proto::ConfigurationAdmissionState::Accepted),
34813545
);
34823546
}
3483-
sandbox.set_phase(SandboxPhase::Ready as i32);
3547+
if runtime_ready {
3548+
ensure_supervisor_ready_status(&mut sandbox.status);
3549+
sandbox.set_phase(SandboxPhase::Ready as i32);
3550+
}
34843551
} else {
34853552
ensure_supervisor_not_ready_status(&mut sandbox.status);
34863553
sandbox.set_phase(SandboxPhase::Provisioning as i32);
@@ -10541,6 +10608,13 @@ mod tests {
1054110608
rejected.status.as_ref().unwrap().configuration_activated,
1054210609
Some(false)
1054310610
);
10611+
assert!(
10612+
runtime
10613+
.supervisor_runtime_ready("sb-1", "supervisor-1")
10614+
.await
10615+
.unwrap_err()
10616+
.contains("not durably admitted")
10617+
);
1054410618

1054510619
admission.state = ConfigurationAdmissionState::Accepted.into();
1054610620
admission.error.clear();
@@ -10554,7 +10628,7 @@ mod tests {
1055410628
.await
1055510629
.unwrap()
1055610630
.unwrap();
10557-
assert_eq!(repaired.phase(), SandboxPhase::Ready as i32);
10631+
assert_eq!(repaired.phase(), SandboxPhase::Provisioning as i32);
1055810632
assert_eq!(
1055910633
repaired.status.as_ref().unwrap().main_process_instance_id,
1056010634
"supervisor-1"
@@ -10563,6 +10637,18 @@ mod tests {
1056310637
repaired.status.as_ref().unwrap().configuration_activated,
1056410638
Some(true)
1056510639
);
10640+
10641+
runtime
10642+
.supervisor_runtime_ready("sb-1", "supervisor-1")
10643+
.await
10644+
.unwrap();
10645+
let ready = runtime
10646+
.store
10647+
.get_message::<Sandbox>("sb-1")
10648+
.await
10649+
.unwrap()
10650+
.unwrap();
10651+
assert_eq!(ready.phase(), SandboxPhase::Ready as i32);
1056610652
}
1056710653

1056810654
#[tokio::test]
@@ -10604,10 +10690,11 @@ mod tests {
1060410690
runtime
1060510691
.set_supervisor_session_state_from_snapshot(
1060610692
"sb-1",
10607-
true,
10608-
Some("test-generation"),
10609-
None,
10610-
false,
10693+
SupervisorSessionStateUpdate::Connected {
10694+
instance_id: "test-generation",
10695+
admission: None,
10696+
readiness: SupervisorRuntimeReadiness::Ready,
10697+
},
1061110698
stale,
1061210699
)
1061310700
.await

crates/openshell-server/src/storage_proto.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ mod tests {
213213
const STORAGE_V1_SCHEMA_SHA256: &str =
214214
"d68401809d8cea445c35233ef32412bbd041cb2ac5acaf368a0d0bf74d2ddf17";
215215
const PUBLIC_RPC_SCHEMA_SHA256: &str =
216-
"2293d2c57a4698fc2b1a7e9bd3470a97931fbbcf7be07c1284bb98a56255454f";
216+
"284412343e72b26b7252e727dd66f2cc530ab5b317eb1ada0b1c3e6fcb80082e";
217217
const DURABLE_SCHEMA_SHA256: &str =
218218
"965a8a09fa80168906a4f9cc6169d3623b98f5281c367695f2f2898230915dd0";
219219
const PUBLIC_DURABLE_OVERLAP_SHA256: &str =
@@ -670,7 +670,7 @@ mod tests {
670670
overlap_hash.as_str(),
671671
),
672672
(
673-
(316, 26),
673+
(317, 26),
674674
(93, 19),
675675
(80, 19),
676676
PUBLIC_RPC_SCHEMA_SHA256,

0 commit comments

Comments
 (0)