Files
codex/codex-rs/exec-server/src/relay_writer.rs
T
Alexi Christakis ddabe594e6 Bound relay connection attempts and handle pongs during blocked writes (#51502)
## Why

A stalled rendezvous WebSocket upgrade can prevent reconnects, while short-lived connections reset reconnect backoff too early. Blocked relay writes also prevent received pongs from clearing their heartbeat deadline.

## What changed

- Bound rendezvous WebSocket connection attempts with `DEFAULT_REMOTE_EXEC_SERVER_CONNECT_TIMEOUT`.
- Retain exponential reconnect backoff across short-lived connections, resetting it only after a connection lasts at least 120 seconds.
- Process pongs during blocked data writes while preserving the in-flight send and its independent write timeout. Prefer queued pongs over an expired heartbeat deadline.
- Abort the relay writer when its owning connection task is dropped.

## Testing

Add coverage for pongs arriving during blocked writes, missing and queued pongs, independent write timeouts, and writer cancellation. Extend reconnect tests to cover stalled WebSocket upgrades without re-registration and backoff reset after a stable session.

GitOrigin-RevId: 0efa350c2b4ada9b89e23df8909063fbb69f33dc
2026-10-06 23:07:25 +00:00

108 lines
4.0 KiB
Rust

//! Keep executor writes bounded while processing pongs independently of write backpressure.
use futures::Sink;
use futures::SinkExt;
use tokio::sync::mpsc;
use tokio::time::Instant;
use tokio_tungstenite::tungstenite::Message;
use tracing::warn;
use crate::connection::WEBSOCKET_KEEPALIVE_INTERVAL;
use crate::relay::RendezvousDisconnectReason;
use crate::websocket_pong_watchdog::WEBSOCKET_PONG_TIMEOUT;
use crate::websocket_pong_watchdog::WebSocketPongWatchdog;
pub(super) async fn run<S, E>(
mut sink: S,
mut outgoing_rx: mpsc::Receiver<Vec<u8>>,
mut pong_rx: mpsc::Receiver<()>,
) -> RendezvousDisconnectReason
where
S: Sink<Message, Error = E> + Unpin,
E: std::fmt::Display,
{
let mut keepalive = tokio::time::interval_at(
Instant::now() + WEBSOCKET_KEEPALIVE_INTERVAL,
WEBSOCKET_KEEPALIVE_INTERVAL,
);
keepalive.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
let mut watchdog = WebSocketPongWatchdog::new(WEBSOCKET_PONG_TIMEOUT);
loop {
let message = tokio::select! {
result = receive_pong(&mut watchdog, &mut pong_rx) => {
if let Err(reason) = result {
return reason;
}
continue;
}
_ = keepalive.tick(), if watchdog.deadline().is_none() => {
Message::Ping(Vec::new().into())
}
encoded = outgoing_rx.recv() => {
let Some(encoded) = encoded else {
return RendezvousDisconnectReason::LocalShutdown;
};
Message::Binary(encoded.into())
}
};
let is_keepalive_ping = matches!(message, Message::Ping(_));
// A pong clears only its own deadline, never the fixed write budget.
let write = tokio::time::timeout(WEBSOCKET_PONG_TIMEOUT, sink.send(message));
tokio::pin!(write);
loop {
tokio::select! {
// Keep the same send future alive when a pong arrives: cancelling
// and restarting a partially flushed send can duplicate a frame.
result = receive_pong(&mut watchdog, &mut pong_rx), if !is_keepalive_ping => {
if let Err(reason) = result {
return reason;
}
}
result = &mut write => {
match result {
Ok(Ok(())) => {
if is_keepalive_ping {
// Pongs received during the ping flush stay queued
// until the watchdog is armed here.
watchdog.ping_sent(Instant::now());
}
break;
}
Ok(Err(error)) => {
warn!("Noise multiplexed environment websocket write failed: {error}");
return RendezvousDisconnectReason::WriteError;
}
Err(_) => {
warn!("Noise multiplexed environment websocket write timed out");
return RendezvousDisconnectReason::WriteError;
}
}
}
}
}
}
}
async fn receive_pong(
watchdog: &mut WebSocketPongWatchdog,
pong_rx: &mut mpsc::Receiver<()>,
) -> Result<(), RendezvousDisconnectReason> {
let deadline = watchdog.deadline();
tokio::select! {
biased;
// Honor a pong already read by the reader even if the timer is also ready.
pong = pong_rx.recv() => {
pong.ok_or(RendezvousDisconnectReason::LocalShutdown)?;
watchdog.received_pong();
Ok(())
}
_ = tokio::time::sleep_until(deadline.unwrap_or_else(Instant::now)), if deadline.is_some() => {
Err(RendezvousDisconnectReason::PongTimeout)
}
}
}
#[cfg(test)]
#[path = "relay_writer_tests.rs"]
mod tests;