Skip to content

Commit 30e32dc

Browse files
committed
refactor(server): internalize terminal finalization state
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
1 parent c9af489 commit 30e32dc

6 files changed

Lines changed: 82 additions & 88 deletions

File tree

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

Lines changed: 13 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2878,12 +2878,16 @@ impl ComputeRuntime {
28782878
sandbox_id: &str,
28792879
instance_id: &str,
28802880
) -> Result<(), String> {
2881-
self.set_supervisor_session_state(sandbox_id, true, Some(instance_id))
2881+
self.set_supervisor_session_state(sandbox_id, true, Some(instance_id), false)
28822882
.await
28832883
}
28842884

2885-
pub async fn supervisor_session_disconnected(&self, sandbox_id: &str) -> Result<(), String> {
2886-
self.set_supervisor_session_state(sandbox_id, false, None)
2885+
pub async fn supervisor_session_disconnected(
2886+
&self,
2887+
sandbox_id: &str,
2888+
terminal_delivery_finalized: bool,
2889+
) -> Result<(), String> {
2890+
self.set_supervisor_session_state(sandbox_id, false, None, terminal_delivery_finalized)
28872891
.await
28882892
}
28892893

@@ -2892,6 +2896,7 @@ impl ComputeRuntime {
28922896
sandbox_id: &str,
28932897
connected: bool,
28942898
instance_id: Option<&str>,
2899+
terminal_delivery_finalized: bool,
28952900
) -> Result<(), String> {
28962901
let guard = self.sync_lock.lock().await;
28972902

@@ -2907,10 +2912,7 @@ impl ComputeRuntime {
29072912
SandboxPhase::try_from(existing.phase()).unwrap_or(SandboxPhase::Unknown);
29082913
if !connected
29092914
&& matches!(current_phase, SandboxPhase::Error | SandboxPhase::Completed)
2910-
&& existing
2911-
.status
2912-
.as_ref()
2913-
.is_some_and(|status| status.main_process_exit_finalized)
2915+
&& terminal_delivery_finalized
29142916
{
29152917
drop(guard);
29162918
self.schedule_ephemeral_sandbox_delete(&existing);
@@ -2941,7 +2943,6 @@ impl ComputeRuntime {
29412943
let status = sandbox.status.get_or_insert_with(Default::default);
29422944
status.main_process_instance_id = instance_id.unwrap_or_default().to_string();
29432945
status.exit_code = None;
2944-
status.main_process_exit_finalized = false;
29452946
sandbox.set_phase(SandboxPhase::Ready as i32);
29462947
} else {
29472948
ensure_supervisor_not_ready_status(&mut sandbox.status, &sandbox_name);
@@ -3087,22 +3088,6 @@ impl ComputeRuntime {
30873088
{
30883089
return Err("main-process instance does not match the terminal result".to_string());
30893090
}
3090-
if status.main_process_exit_finalized {
3091-
return Ok(());
3092-
}
3093-
let expected_resource_version = sandbox_resource_version(&sandbox);
3094-
let finalized = self
3095-
.store
3096-
.update_message_cas::<Sandbox, _>(sandbox_id, expected_resource_version, |sandbox| {
3097-
sandbox
3098-
.status
3099-
.get_or_insert_with(Default::default)
3100-
.main_process_exit_finalized = true;
3101-
})
3102-
.await
3103-
.map_err(|error| error.to_string())?;
3104-
self.sandbox_index.update_from_sandbox(&finalized);
3105-
self.sandbox_watch_bus.notify(sandbox_id);
31063091
Ok(())
31073092
}
31083093

@@ -3499,7 +3484,6 @@ fn apply_main_process_exit(sandbox: &mut Sandbox, instance_id: &str, exit_code:
34993484
});
35003485
status.main_process_instance_id = instance_id.to_string();
35013486
status.exit_code = Some(exit_code);
3502-
status.main_process_exit_finalized = false;
35033487
if preserve_infrastructure_error {
35043488
return;
35053489
}
@@ -3910,7 +3894,6 @@ fn public_status_from_driver(
39103894
current_policy_version,
39113895
main_process_instance_id: String::new(),
39123896
exit_code: None,
3913-
main_process_exit_finalized: false,
39143897
}
39153898
}
39163899

@@ -5334,19 +5317,8 @@ mod tests {
53345317
.await
53355318
.unwrap();
53365319
assert_eq!(driver.delete_calls(), 0);
5337-
assert!(
5338-
runtime
5339-
.store
5340-
.get_message::<Sandbox>("sb-1")
5341-
.await
5342-
.unwrap()
5343-
.unwrap()
5344-
.status
5345-
.unwrap()
5346-
.main_process_exit_finalized
5347-
);
53485320
runtime
5349-
.supervisor_session_disconnected("sb-1")
5321+
.supervisor_session_disconnected("sb-1", true)
53505322
.await
53515323
.unwrap();
53525324
tokio::time::timeout(Duration::from_secs(1), async {
@@ -7054,7 +7026,7 @@ mod tests {
70547026
let mut watch_rx = runtime.sandbox_watch_bus.subscribe("sb-1");
70557027

70567028
runtime
7057-
.supervisor_session_disconnected("sb-1")
7029+
.supervisor_session_disconnected("sb-1", false)
70587030
.await
70597031
.unwrap();
70607032

@@ -8329,7 +8301,7 @@ mod tests {
83298301
runtime.store.put_message(&sandbox).await.unwrap();
83308302

83318303
runtime
8332-
.supervisor_session_disconnected("sb-1")
8304+
.supervisor_session_disconnected("sb-1", false)
83338305
.await
83348306
.unwrap();
83358307

@@ -8573,7 +8545,7 @@ mod tests {
85738545
// Session drops.
85748546
runtime.supervisor_sessions.cleanup_sandbox("sb-1");
85758547
runtime
8576-
.supervisor_session_disconnected("sb-1")
8548+
.supervisor_session_disconnected("sb-1", false)
85778549
.await
85788550
.unwrap();
85798551
let stored = runtime

crates/openshell-server/src/grpc/sandbox.rs

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1715,11 +1715,10 @@ fn sandbox_relay_reachable(state: &ServerState, sandbox: &Sandbox) -> bool {
17151715
let phase = SandboxPhase::try_from(sandbox.phase()).ok();
17161716
matches!(phase, Some(SandboxPhase::Ready))
17171717
|| (matches!(phase, Some(SandboxPhase::Completed | SandboxPhase::Error))
1718-
&& sandbox
1719-
.status
1720-
.as_ref()
1721-
.is_some_and(|status| !status.main_process_exit_finalized)
1722-
&& state.supervisor_sessions.has_session(sandbox.object_id()))
1718+
&& state.supervisor_sessions.has_session(sandbox.object_id())
1719+
&& !state
1720+
.supervisor_sessions
1721+
.terminal_delivery_finalized(sandbox.object_id()))
17231722
}
17241723

17251724
pub(super) async fn handle_create_ssh_session(
@@ -4009,13 +4008,12 @@ mod tests {
40094008

40104009
assert!(response.is_ok());
40114010

4012-
let mut finalized = sandbox;
4013-
finalized
4014-
.status
4015-
.as_mut()
4016-
.expect("sandbox status")
4017-
.main_process_exit_finalized = true;
4018-
assert!(!sandbox_relay_reachable(&state, &finalized));
4011+
assert!(
4012+
state
4013+
.supervisor_sessions
4014+
.finalize_main_process_exit("sandbox-work")
4015+
);
4016+
assert!(!sandbox_relay_reachable(&state, &sandbox));
40194017
}
40204018

40214019
#[tokio::test]

crates/openshell-server/src/supervisor_session.rs

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ struct LiveSession {
5858
/// the old session's `tx` just before supersede could still enqueue a
5959
/// `RelayOpen` onto the stale stream and sit until the relay timeout.
6060
shutdown: oneshot::Sender<()>,
61+
/// Set after the supervisor confirms that every expected foreground
62+
/// attachment has closed and terminal output delivery is complete.
63+
terminal_delivery_finalized: bool,
6164
#[allow(dead_code)]
6265
connected_at: Instant,
6366
}
@@ -125,6 +128,7 @@ impl SupervisorSessionRegistry {
125128
session_id,
126129
tx,
127130
shutdown,
131+
terminal_delivery_finalized: false,
128132
connected_at: Instant::now(),
129133
},
130134
);
@@ -163,15 +167,17 @@ impl SupervisorSessionRegistry {
163167
/// This guards against the supersede race: an old session's task may
164168
/// finish long after a new session has taken its place. The old task's
165169
/// cleanup must not evict the new registration.
166-
fn remove_if_current(&self, sandbox_id: &str, session_id: &str) -> bool {
170+
fn remove_if_current(&self, sandbox_id: &str, session_id: &str) -> Option<bool> {
167171
let mut sessions = self.sessions.lock().unwrap();
168172
let is_current = sessions
169173
.get(sandbox_id)
170174
.is_some_and(|s| s.session_id == session_id);
171175
if is_current {
172-
sessions.remove(sandbox_id);
176+
return sessions
177+
.remove(sandbox_id)
178+
.map(|session| session.terminal_delivery_finalized);
173179
}
174-
is_current
180+
None
175181
}
176182

177183
/// Look up the sender for a supervisor session, waiting up to `timeout`
@@ -210,6 +216,23 @@ impl SupervisorSessionRegistry {
210216
self.sessions.lock().unwrap().contains_key(sandbox_id)
211217
}
212218

219+
pub fn terminal_delivery_finalized(&self, sandbox_id: &str) -> bool {
220+
self.sessions
221+
.lock()
222+
.unwrap()
223+
.get(sandbox_id)
224+
.is_some_and(|session| session.terminal_delivery_finalized)
225+
}
226+
227+
pub fn finalize_main_process_exit(&self, sandbox_id: &str) -> bool {
228+
let mut sessions = self.sessions.lock().unwrap();
229+
let Some(session) = sessions.get_mut(sandbox_id) else {
230+
return false;
231+
};
232+
session.terminal_delivery_finalized = true;
233+
true
234+
}
235+
213236
pub fn is_current_session(&self, sandbox_id: &str, session_id: &str) -> bool {
214237
self.sessions
215238
.lock()
@@ -797,17 +820,17 @@ pub async fn handle_connect_supervisor(
797820
shutdown_rx,
798821
)
799822
.await;
800-
let still_ours = state_clone
823+
let terminal_finalized = state_clone
801824
.supervisor_sessions
802825
.remove_if_current(&sandbox_id_clone, &session_id);
803-
if still_ours {
826+
if let Some(terminal_finalized) = terminal_finalized {
804827
info!(sandbox_id = %sandbox_id_clone, session_id = %session_id, "supervisor session: ended");
805828
state_clone
806829
.telemetry
807830
.sandbox_session_disconnected(&sandbox_id_clone);
808831
if let Err(err) = state_clone
809832
.compute
810-
.supervisor_session_disconnected(&sandbox_id_clone)
833+
.supervisor_session_disconnected(&sandbox_id_clone, terminal_finalized)
811834
.await
812835
{
813836
warn!(
@@ -874,6 +897,14 @@ pub async fn handle_finalize_main_process_exit(
874897
.finalize_main_process_exit(&report.sandbox_id, &report.instance_id)
875898
.await
876899
.map_err(Status::failed_precondition)?;
900+
if !state
901+
.supervisor_sessions
902+
.finalize_main_process_exit(&report.sandbox_id)
903+
{
904+
return Err(Status::failed_precondition(
905+
"supervisor session is not connected",
906+
));
907+
}
877908
Ok(Response::new(
878909
openshell_core::proto::FinalizeMainProcessExitResponse {},
879910
))
@@ -1133,7 +1164,7 @@ mod tests {
11331164
let (tx, _rx) = mpsc::channel(1);
11341165
registry.register("sbx".to_string(), "s1".to_string(), tx, make_shutdown());
11351166

1136-
assert!(registry.remove_if_current("sbx", "s1"));
1167+
assert_eq!(registry.remove_if_current("sbx", "s1"), Some(false));
11371168
assert!(!registry.sessions.lock().unwrap().contains_key("sbx"));
11381169
}
11391170

@@ -1159,7 +1190,7 @@ mod tests {
11591190

11601191
// Cleanup from the old session task runs late. It must NOT evict the
11611192
// newly registered session.
1162-
assert!(!registry.remove_if_current("sbx", "s-old"));
1193+
assert_eq!(registry.remove_if_current("sbx", "s-old"), None);
11631194
let sessions = registry.sessions.lock().unwrap();
11641195
assert!(
11651196
sessions.contains_key("sbx"),
@@ -1171,7 +1202,18 @@ mod tests {
11711202
#[test]
11721203
fn remove_if_current_unknown_sandbox_is_noop() {
11731204
let registry = SupervisorSessionRegistry::new();
1174-
assert!(!registry.remove_if_current("sbx-does-not-exist", "s1"));
1205+
assert_eq!(registry.remove_if_current("sbx-does-not-exist", "s1"), None);
1206+
}
1207+
1208+
#[test]
1209+
fn remove_if_current_returns_terminal_finalization_state() {
1210+
let registry = SupervisorSessionRegistry::new();
1211+
let (tx, _rx) = mpsc::channel(1);
1212+
registry.register("sbx".to_string(), "s1".to_string(), tx, make_shutdown());
1213+
1214+
assert!(registry.finalize_main_process_exit("sbx"));
1215+
assert!(registry.terminal_delivery_finalized("sbx"));
1216+
assert_eq!(registry.remove_if_current("sbx", "s1"), Some(true));
11751217
}
11761218

11771219
// ---- open_relay: happy path and wait semantics ----

proto/openshell.proto

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -918,9 +918,6 @@ message SandboxStatus {
918918
// Presence indicates that the canonical main process exited. Exit code 0
919919
// produces Completed; nonzero and signal-normalized exits produce Error.
920920
optional int32 exit_code = 9;
921-
// True after every expected foreground main-process attachment has closed
922-
// naturally and the supervisor is ready to exit.
923-
bool main_process_exit_finalized = 10;
924921
}
925922

926923
// User-facing sandbox condition derived from driver-native conditions.

sdk/go/openshell/v1/internal/converter/coverage_test.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,9 @@ func TestConverterCoversAllProtoFields_SandboxStatus(t *testing.T) {
6363
"current_policy_version": true,
6464
"exit_code": true,
6565
}
66-
// These fields coordinate internal gateway/supervisor lifecycle fencing and
67-
// terminal delivery. They are exposed only through the raw protobuf API.
68-
skipped := fieldSet{
69-
"main_process_instance_id": true,
70-
"main_process_exit_finalized": true,
71-
}
66+
// The instance ID coordinates internal gateway/supervisor lifecycle
67+
// fencing. It is exposed only through the raw protobuf API.
68+
skipped := fieldSet{"main_process_instance_id": true}
7269

7370
assertAllFieldsCovered(t, (&pb.SandboxStatus{}).ProtoReflect().Descriptor(), handled, skipped)
7471
}

sdk/go/proto/openshellv1/openshell.pb.go

Lines changed: 5 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)