Skip to content

Commit aaa901f

Browse files
committed
fix(server): replay relays safely after reconnect
Signed-off-by: Evan Lezar <elezar@nvidia.com>
1 parent b410514 commit aaa901f

2 files changed

Lines changed: 125 additions & 23 deletions

File tree

‎architecture/gateway.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,10 @@ the serving gateway retries ownership lookup until the normal relay wait
410410
deadline. Each retry re-reads the owner record, so a supervisor reconnect or
411411
heartbeat can surface a new owner; if no fresh reachable owner appears before
412412
the deadline, the client operation fails rather than electing an owner itself.
413+
The owning gateway replays unclaimed relay opens when a supervisor reconnects,
414+
including when the previous session ended before the new one registered. Relay
415+
delivery is tracked by session ID so an open queued during session setup is not
416+
sent twice to the same supervisor connection.
413417
Provider-readiness reports, endpoint-status reports, and provider-status reads
414418
also follow the durable owner record through unary peer RPCs. The owning replica
415419
validates the current supervisor session and keeps the in-memory evidence; a

‎crates/openshell-server/src/supervisor_session.rs‎

Lines changed: 121 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,9 @@ struct PendingRelay {
335335
sender: RelayStreamSender,
336336
sandbox_id: String,
337337
relay_open: RelayOpen,
338+
/// Session that most recently received this relay open. Reconnect replay
339+
/// skips relays already sent to the session being established.
340+
last_sent_session_id: Option<String>,
338341
created_at: Instant,
339342
}
340343

@@ -474,13 +477,13 @@ impl SupervisorSessionRegistry {
474477
&self,
475478
sandbox_id: &str,
476479
timeout: Duration,
477-
) -> Result<mpsc::Sender<GatewayMessage>, Status> {
480+
) -> Result<(String, mpsc::Sender<GatewayMessage>), Status> {
478481
let deadline = Instant::now() + timeout;
479482
let mut backoff = SESSION_WAIT_INITIAL_BACKOFF;
480483

481484
loop {
482-
if let Some(tx) = self.lookup_session(sandbox_id) {
483-
return Ok(tx);
485+
if let Some(session) = self.lookup_session_with_id(sandbox_id) {
486+
return Ok(session);
484487
}
485488
if Instant::now() + backoff > deadline {
486489
return Err(Status::unavailable("supervisor session not connected"));
@@ -490,12 +493,15 @@ impl SupervisorSessionRegistry {
490493
}
491494
}
492495

493-
fn lookup_session(&self, sandbox_id: &str) -> Option<mpsc::Sender<GatewayMessage>> {
496+
fn lookup_session_with_id(
497+
&self,
498+
sandbox_id: &str,
499+
) -> Option<(String, mpsc::Sender<GatewayMessage>)> {
494500
self.sessions
495501
.lock()
496502
.unwrap()
497503
.get(sandbox_id)
498-
.map(|s| s.tx.clone())
504+
.map(|s| (s.session_id.clone(), s.tx.clone()))
499505
}
500506

501507
pub fn has_session(&self, sandbox_id: &str) -> bool {
@@ -791,7 +797,7 @@ impl SupervisorSessionRegistry {
791797
if relay_open.channel_id.is_empty() {
792798
return Err(Status::invalid_argument("relay channel_id is required"));
793799
}
794-
let tx = self
800+
let (session_id, tx) = self
795801
.wait_for_session(sandbox_id, session_wait_timeout)
796802
.await?;
797803

@@ -824,6 +830,7 @@ impl SupervisorSessionRegistry {
824830
sender: relay_tx,
825831
sandbox_id: sandbox_id.to_string(),
826832
relay_open: relay_open.clone(),
833+
last_sent_session_id: Some(session_id),
827834
created_at: Instant::now(),
828835
},
829836
);
@@ -918,22 +925,29 @@ impl SupervisorSessionRegistry {
918925
self.remove(sandbox_id);
919926
}
920927

921-
pub async fn replay_pending_relays(&self, sandbox_id: &str, tx: &mpsc::Sender<GatewayMessage>) {
928+
pub async fn replay_pending_relays(
929+
&self,
930+
sandbox_id: &str,
931+
session_id: &str,
932+
tx: &mpsc::Sender<GatewayMessage>,
933+
) {
922934
for channel_id in self.pending_channel_ids(sandbox_id) {
923935
let relay_open = {
924-
let pending = self.pending_relays.lock().unwrap();
925-
pending
926-
.get(&channel_id)
927-
.map(|pending| pending.relay_open.clone())
928-
};
929-
let Some(relay_open) = relay_open else {
930-
continue;
936+
let mut pending = self.pending_relays.lock().unwrap();
937+
let Some(pending) = pending.get_mut(&channel_id) else {
938+
continue;
939+
};
940+
if pending.last_sent_session_id.as_deref() == Some(session_id) {
941+
continue;
942+
}
943+
pending.last_sent_session_id = Some(session_id.to_string());
944+
pending.relay_open.clone()
931945
};
932946
let msg = GatewayMessage {
933947
payload: Some(gateway_message::Payload::RelayOpen(relay_open)),
934948
};
935949
if tx.send(msg).await.is_err() {
936-
warn!(sandbox_id = %sandbox_id, channel_id = %channel_id, "supervisor session: failed to replay pending relay to superseding session");
950+
warn!(sandbox_id = %sandbox_id, channel_id = %channel_id, "supervisor session: failed to replay pending relay to new session");
937951
break;
938952
}
939953
}
@@ -1960,12 +1974,12 @@ async fn establish_supervisor_session(
19601974
}
19611975
state.telemetry.sandbox_session_connected(&sandbox_id);
19621976

1963-
if superseded {
1964-
state
1965-
.supervisor_sessions
1966-
.replay_pending_relays(&sandbox_id, &tx)
1967-
.await;
1968-
}
1977+
// The previous session may already have been removed before this one
1978+
// registers. Pending relay opens still need to reach the new session.
1979+
state
1980+
.supervisor_sessions
1981+
.replay_pending_relays(&sandbox_id, &session_id, &tx)
1982+
.await;
19691983

19701984
// Step 4: Spawn the session loop that reads inbound messages.
19711985
let state_clone = Arc::clone(&state);
@@ -2495,6 +2509,7 @@ mod tests {
24952509
target: Some(relay_open::Target::Ssh(SshRelayTarget {})),
24962510
service_id: String::new(),
24972511
},
2512+
last_sent_session_id: None,
24982513
created_at,
24992514
}
25002515
}
@@ -2950,13 +2965,13 @@ mod tests {
29502965
let superseded = registry.register(
29512966
"sbx".to_string(),
29522967
"s-new".to_string(),
2953-
tx_new,
2968+
tx_new.clone(),
29542969
make_shutdown(),
29552970
);
29562971
assert!(superseded);
29572972

29582973
registry
2959-
.replay_pending_relays("sbx", &registry.lookup_session("sbx").unwrap())
2974+
.replay_pending_relays("sbx", "s-new", &tx_new)
29602975
.await;
29612976

29622977
let replayed = rx_new
@@ -2971,6 +2986,89 @@ mod tests {
29712986
}
29722987
}
29732988

2989+
#[tokio::test]
2990+
async fn replay_pending_relays_reissues_open_after_disconnected_session() {
2991+
let registry = SupervisorSessionRegistry::new();
2992+
let (tx_old, mut rx_old) = mpsc::channel::<GatewayMessage>(4);
2993+
let (tx_new, mut rx_new) = mpsc::channel::<GatewayMessage>(4);
2994+
2995+
registry.register(
2996+
"sbx".to_string(),
2997+
"s-old".to_string(),
2998+
tx_old,
2999+
make_shutdown(),
3000+
);
3001+
let (channel_id, _relay_rx) = registry
3002+
.open_relay("sbx", Duration::from_secs(1))
3003+
.await
3004+
.expect("open_relay should succeed");
3005+
rx_old
3006+
.recv()
3007+
.await
3008+
.expect("old session should receive RelayOpen");
3009+
3010+
assert_eq!(registry.remove_if_current("sbx", "s-old"), Some(false));
3011+
let superseded = registry.register(
3012+
"sbx".to_string(),
3013+
"s-new".to_string(),
3014+
tx_new.clone(),
3015+
make_shutdown(),
3016+
);
3017+
assert!(!superseded, "old session was removed before reconnect");
3018+
3019+
registry
3020+
.replay_pending_relays("sbx", "s-new", &tx_new)
3021+
.await;
3022+
3023+
let replayed = rx_new
3024+
.recv()
3025+
.await
3026+
.expect("new session should receive RelayOpen");
3027+
match replayed.payload {
3028+
Some(gateway_message::Payload::RelayOpen(open)) => {
3029+
assert_eq!(open.channel_id, channel_id);
3030+
}
3031+
other => panic!("expected RelayOpen, got {other:?}"),
3032+
}
3033+
}
3034+
3035+
#[tokio::test]
3036+
async fn replay_pending_relays_skips_open_already_sent_to_session() {
3037+
let registry = SupervisorSessionRegistry::new();
3038+
let (tx, mut rx) = mpsc::channel::<GatewayMessage>(4);
3039+
3040+
registry.register(
3041+
"sbx".to_string(),
3042+
"s-new".to_string(),
3043+
tx.clone(),
3044+
make_shutdown(),
3045+
);
3046+
3047+
let (channel_id, _relay_rx) = registry
3048+
.open_relay("sbx", Duration::from_secs(1))
3049+
.await
3050+
.expect("open_relay should succeed");
3051+
let original = rx
3052+
.recv()
3053+
.await
3054+
.expect("new session should receive RelayOpen");
3055+
assert!(matches!(
3056+
original.payload,
3057+
Some(gateway_message::Payload::RelayOpen(ref open))
3058+
if open.channel_id == channel_id
3059+
));
3060+
3061+
// Model establish_supervisor_session reaching its unconditional replay
3062+
// after open_relay observed the newly registered session.
3063+
registry.replay_pending_relays("sbx", "s-new", &tx).await;
3064+
3065+
let duplicate = tokio::time::timeout(Duration::from_millis(50), rx.recv()).await;
3066+
assert!(
3067+
duplicate.is_err(),
3068+
"RelayOpen for {channel_id} was delivered twice: {duplicate:?}"
3069+
);
3070+
}
3071+
29743072
#[tokio::test]
29753073
async fn require_persisted_sandbox_rejects_missing_sandbox() {
29763074
let store = test_store().await;

0 commit comments

Comments
 (0)