mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
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
This commit is contained in:
committed by
copyberry
parent
7298a6e020
commit
ddabe594e6
@@ -15,6 +15,7 @@ use tokio::task::JoinSet;
|
||||
use tokio::time::Instant;
|
||||
use tokio::time::timeout;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use tokio_util::task::AbortOnDropHandle;
|
||||
use tracing::debug;
|
||||
use tracing::info;
|
||||
use tracing::warn;
|
||||
@@ -46,9 +47,10 @@ use crate::relay_proto::relay_message_frame;
|
||||
#[cfg(test)]
|
||||
use crate::server::ConnectionProcessor;
|
||||
use crate::telemetry::ExecutorRegistration;
|
||||
use crate::websocket_pong_watchdog::WEBSOCKET_PONG_TIMEOUT;
|
||||
use crate::websocket_pong_watchdog::WEBSOCKET_PONG_TIMEOUT_REASON;
|
||||
use crate::websocket_pong_watchdog::WebSocketPongWatchdog;
|
||||
|
||||
#[path = "relay_writer.rs"]
|
||||
mod writer;
|
||||
|
||||
const RELAY_MESSAGE_FRAME_VERSION: u32 = 1;
|
||||
const MAX_ACTIVE_NOISE_RELAY_STREAMS: usize = 128;
|
||||
@@ -526,77 +528,17 @@ where
|
||||
let executor_registration =
|
||||
ExecutorRegistration::new(environment_id.clone(), executor_registration_id.clone())
|
||||
.map(Arc::new);
|
||||
let (mut websocket_sink, mut websocket_stream) = stream.split();
|
||||
let (physical_outgoing_tx, mut physical_outgoing_rx) =
|
||||
mpsc::channel::<Vec<u8>>(CHANNEL_CAPACITY);
|
||||
let (websocket_sink, mut websocket_stream) = stream.split();
|
||||
let (physical_outgoing_tx, physical_outgoing_rx) = mpsc::channel::<Vec<u8>>(CHANNEL_CAPACITY);
|
||||
let (closed_stream_tx, mut closed_stream_rx) =
|
||||
mpsc::channel::<ClosedNoiseVirtualStream>(MAX_ACTIVE_NOISE_RELAY_STREAMS);
|
||||
let (pong_tx, mut pong_rx) = mpsc::channel(1);
|
||||
let (pong_tx, pong_rx) = mpsc::channel(1);
|
||||
// Use a separate writer so this loop never waits on the channel it drains.
|
||||
let mut physical_writer_task = tokio::spawn(async move {
|
||||
let mut keepalive = tokio::time::interval_at(
|
||||
tokio::time::Instant::now() + WEBSOCKET_KEEPALIVE_INTERVAL,
|
||||
WEBSOCKET_KEEPALIVE_INTERVAL,
|
||||
);
|
||||
keepalive.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
|
||||
let mut pong_watchdog = WebSocketPongWatchdog::new(WEBSOCKET_PONG_TIMEOUT);
|
||||
let pong_deadline = tokio::time::sleep(WEBSOCKET_PONG_TIMEOUT);
|
||||
tokio::pin!(pong_deadline);
|
||||
loop {
|
||||
let message = tokio::select! {
|
||||
pong = pong_rx.recv() => {
|
||||
let Some(()) = pong else {
|
||||
break RendezvousDisconnectReason::LocalShutdown;
|
||||
};
|
||||
pong_watchdog.received_pong();
|
||||
continue;
|
||||
}
|
||||
_ = &mut pong_deadline, if pong_watchdog.deadline().is_some() => {
|
||||
match pong_rx.try_recv() {
|
||||
Ok(()) => {
|
||||
pong_watchdog.received_pong();
|
||||
continue;
|
||||
}
|
||||
Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {
|
||||
break RendezvousDisconnectReason::PongTimeout;
|
||||
}
|
||||
Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {
|
||||
break RendezvousDisconnectReason::LocalShutdown;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = keepalive.tick(), if pong_watchdog.deadline().is_none() => {
|
||||
Message::Ping(Vec::new().into())
|
||||
}
|
||||
encoded = physical_outgoing_rx.recv() => {
|
||||
let Some(encoded) = encoded else {
|
||||
break RendezvousDisconnectReason::LocalShutdown;
|
||||
};
|
||||
Message::Binary(encoded.into())
|
||||
}
|
||||
};
|
||||
let is_keepalive_ping = matches!(message, Message::Ping(_));
|
||||
let write_deadline = pong_watchdog.write_deadline(tokio::time::Instant::now());
|
||||
match tokio::time::timeout_at(write_deadline, websocket_sink.send(message)).await {
|
||||
Ok(Ok(())) => {
|
||||
if is_keepalive_ping {
|
||||
pong_watchdog.ping_sent(tokio::time::Instant::now());
|
||||
if let Some(deadline) = pong_watchdog.deadline() {
|
||||
pong_deadline.as_mut().reset(deadline);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Err(error)) => {
|
||||
warn!("Noise multiplexed environment websocket write failed: {error}");
|
||||
break RendezvousDisconnectReason::WriteError;
|
||||
}
|
||||
Err(_) => {
|
||||
warn!("Noise multiplexed environment websocket write timed out");
|
||||
break RendezvousDisconnectReason::WriteError;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let mut physical_writer_task = AbortOnDropHandle::new(tokio::spawn(writer::run(
|
||||
websocket_sink,
|
||||
physical_outgoing_rx,
|
||||
pong_rx,
|
||||
)));
|
||||
let mut streams: HashMap<String, NoiseVirtualStream<H>> = HashMap::new();
|
||||
let mut pending_handshakes: HashMap<String, PendingHandshake> = HashMap::new();
|
||||
let mut validation_tasks: JoinSet<HarnessKeyValidationResult> = JoinSet::new();
|
||||
@@ -1113,9 +1055,11 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multiplexed_environment_sends_keepalive() -> anyhow::Result<()> {
|
||||
let (client_websocket, mut server_websocket) = websocket_pair().await?;
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn multiplexed_environment_sends_keepalive_and_cancels_blocked_writer()
|
||||
-> anyhow::Result<()> {
|
||||
let (client_websocket, control, mut outbound_rx) =
|
||||
ControlledWebSocket::new(/*write_ready*/ true);
|
||||
let runtime_paths = crate::ExecServerRuntimeOptions::new(
|
||||
std::env::current_exe()?,
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
@@ -1130,10 +1074,17 @@ mod tests {
|
||||
AllowHarnessKeyValidator,
|
||||
));
|
||||
|
||||
read_keepalive_ping(&mut server_websocket).await?;
|
||||
assert!(matches!(outbound_rx.next().await, Some(Message::Ping(_))));
|
||||
control.set_write_blocked();
|
||||
control.send_inbound(Message::Pong(Vec::new().into()))?;
|
||||
control.wait_for_blocked_write().await?;
|
||||
|
||||
environment_task.abort();
|
||||
let _ = environment_task.await;
|
||||
assert_eq!(
|
||||
timeout(Duration::from_millis(1), outbound_rx.next()).await?,
|
||||
None
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
//! 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;
|
||||
@@ -0,0 +1,98 @@
|
||||
//! Exercise heartbeat deadlines while a single physical write is stalled.
|
||||
|
||||
use std::convert::Infallible;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use tokio::sync::Notify;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_util::task::AbortOnDropHandle;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn pong_during_write_clears_only_the_pong_deadline() {
|
||||
for release_write in [true, false] {
|
||||
let (outgoing_tx, outgoing_rx) = mpsc::channel(1);
|
||||
let (pong_tx, pong_rx) = mpsc::channel(1);
|
||||
let (observed_tx, mut observed_rx) = mpsc::unbounded_channel();
|
||||
let release = Arc::new(Notify::new());
|
||||
let completed = Arc::new(Notify::new());
|
||||
let sink = futures::sink::unfold(
|
||||
(observed_tx, Arc::clone(&release), Arc::clone(&completed)),
|
||||
|(observed_tx, release, completed), message| async move {
|
||||
let is_data = matches!(message, Message::Binary(_));
|
||||
observed_tx.send(message).unwrap();
|
||||
if is_data {
|
||||
release.notified().await;
|
||||
completed.notify_one();
|
||||
}
|
||||
Ok::<_, Infallible>((observed_tx, release, completed))
|
||||
},
|
||||
);
|
||||
let task = AbortOnDropHandle::new(tokio::spawn(run(Box::pin(sink), outgoing_rx, pong_rx)));
|
||||
assert!(matches!(observed_rx.recv().await, Some(Message::Ping(_))));
|
||||
tokio::time::advance(WEBSOCKET_PONG_TIMEOUT * 3 / 4).await;
|
||||
outgoing_tx.send(vec![42]).await.unwrap();
|
||||
assert_eq!(
|
||||
observed_rx.recv().await,
|
||||
Some(Message::Binary(vec![42].into()))
|
||||
);
|
||||
pong_tx.send(()).await.unwrap();
|
||||
|
||||
// Let the old pong deadline expire while the write remains blocked.
|
||||
tokio::time::advance(WEBSOCKET_PONG_TIMEOUT / 2).await;
|
||||
tokio::task::yield_now().await;
|
||||
assert!(!task.is_finished());
|
||||
if release_write {
|
||||
release.notify_one();
|
||||
completed.notified().await;
|
||||
drop(outgoing_tx);
|
||||
drop(pong_tx);
|
||||
assert_eq!(
|
||||
task.await.unwrap(),
|
||||
RendezvousDisconnectReason::LocalShutdown
|
||||
);
|
||||
} else {
|
||||
// Receiving a pong must not extend the independent write budget.
|
||||
tokio::time::advance(WEBSOCKET_PONG_TIMEOUT).await;
|
||||
assert_eq!(task.await.unwrap(), RendezvousDisconnectReason::WriteError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn missing_pong_times_out_during_a_blocked_write() {
|
||||
let (outgoing_tx, outgoing_rx) = mpsc::channel(1);
|
||||
let (_pong_tx, pong_rx) = mpsc::channel(1);
|
||||
let (observed_tx, mut observed_rx) = mpsc::unbounded_channel();
|
||||
let sink = futures::sink::unfold(observed_tx, |observed_tx, message| async move {
|
||||
let is_data = matches!(message, Message::Binary(_));
|
||||
observed_tx.send(message).unwrap();
|
||||
if is_data {
|
||||
std::future::pending::<()>().await;
|
||||
}
|
||||
Ok::<_, Infallible>(observed_tx)
|
||||
});
|
||||
let task = AbortOnDropHandle::new(tokio::spawn(run(Box::pin(sink), outgoing_rx, pong_rx)));
|
||||
assert!(matches!(observed_rx.recv().await, Some(Message::Ping(_))));
|
||||
tokio::time::advance(WEBSOCKET_PONG_TIMEOUT * 3 / 4).await;
|
||||
outgoing_tx.send(vec![42]).await.unwrap();
|
||||
assert_eq!(
|
||||
observed_rx.recv().await,
|
||||
Some(Message::Binary(vec![42].into()))
|
||||
);
|
||||
assert_eq!(task.await.unwrap(), RendezvousDisconnectReason::PongTimeout);
|
||||
}
|
||||
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn queued_pong_wins_over_an_expired_deadline() {
|
||||
let (pong_tx, mut pong_rx) = mpsc::channel(1);
|
||||
let mut watchdog = WebSocketPongWatchdog::new(Duration::from_secs(1));
|
||||
watchdog.ping_sent(Instant::now());
|
||||
tokio::time::advance(Duration::from_secs(1)).await;
|
||||
pong_tx.send(()).await.unwrap();
|
||||
assert_eq!(receive_pong(&mut watchdog, &mut pong_rx).await, Ok(()));
|
||||
assert_eq!(watchdog.deadline(), None);
|
||||
}
|
||||
@@ -709,7 +709,7 @@ async fn run_remote_environment_connections<H: NoiseStreamHandler>(
|
||||
let identity = NoiseChannelIdentity::generate().map_err(|error| {
|
||||
ExecServerError::Protocol(format!("failed to generate Noise relay identity: {error}"))
|
||||
})?;
|
||||
let mut backoff = Duration::from_secs(1);
|
||||
let mut backoff = reconnect_backoff::ReconnectBackoff::new();
|
||||
let mut response = client
|
||||
.register_environment_with_retry(&config.environment_id, &identity.public_key())
|
||||
.await?;
|
||||
@@ -727,7 +727,7 @@ async fn run_remote_environment_connections<H: NoiseStreamHandler>(
|
||||
.await
|
||||
{
|
||||
Ok(websocket) => {
|
||||
backoff = Duration::from_secs(1);
|
||||
let connected_at = tokio::time::Instant::now();
|
||||
let executor_registration_id = response.executor_registration_id.clone();
|
||||
info!(
|
||||
noise_event = "rendezvous_connection",
|
||||
@@ -749,6 +749,7 @@ async fn run_remote_environment_connections<H: NoiseStreamHandler>(
|
||||
},
|
||||
)
|
||||
.await;
|
||||
backoff.connection_closed(connected_at.elapsed());
|
||||
info!(
|
||||
noise_event = "rendezvous_connection",
|
||||
noise_outcome = "disconnected",
|
||||
@@ -794,12 +795,7 @@ async fn run_remote_environment_connections<H: NoiseStreamHandler>(
|
||||
}
|
||||
}
|
||||
|
||||
sleep(reconnect_backoff::reconnect_delay(
|
||||
backoff,
|
||||
uuid::Uuid::new_v4().as_u64_pair().1,
|
||||
))
|
||||
.await;
|
||||
backoff = (backoff * 2).min(Duration::from_secs(30));
|
||||
sleep(backoff.next_delay(uuid::Uuid::new_v4().as_u64_pair().1)).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -820,7 +816,7 @@ async fn connect_rendezvous(
|
||||
http_client_factory: &HttpClientFactory,
|
||||
) -> Result<WebSocketConnection, tokio_tungstenite::tungstenite::Error> {
|
||||
let started_at = Instant::now();
|
||||
let result = async {
|
||||
let result = tokio::time::timeout(DEFAULT_REMOTE_EXEC_SERVER_CONNECT_TIMEOUT, async {
|
||||
let mut request = url.into_client_request()?;
|
||||
request.headers_mut().extend(current_rendezvous_headers());
|
||||
request
|
||||
@@ -836,8 +832,16 @@ async fn connect_rendezvous(
|
||||
.connect(request, noise_relay_websocket_config())
|
||||
.await
|
||||
.map(|(websocket, _)| websocket)
|
||||
}
|
||||
.await;
|
||||
})
|
||||
.await
|
||||
.unwrap_or_else(|_| {
|
||||
Err(tokio_tungstenite::tungstenite::Error::Io(
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::TimedOut,
|
||||
"rendezvous websocket connection timed out",
|
||||
),
|
||||
))
|
||||
});
|
||||
let result_name = if result.is_ok() { "success" } else { "error" };
|
||||
tracing::Span::current().record("result", result_name);
|
||||
telemetry.remote_rendezvous_completed(result_name, started_at.elapsed());
|
||||
|
||||
@@ -186,22 +186,32 @@ async fn reconnect_reuses_registration_until_url_is_rejected() -> Result<()> {
|
||||
static_registry_auth_provider(),
|
||||
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
|
||||
)?;
|
||||
let environment_task = tokio::spawn(run_remote_environment(
|
||||
config,
|
||||
ExecServerRuntimeOptions::new(
|
||||
std::env::current_exe()?,
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
)?,
|
||||
));
|
||||
let environment_task =
|
||||
tokio_util::task::AbortOnDropHandle::new(tokio::spawn(run_remote_environment(
|
||||
config,
|
||||
ExecServerRuntimeOptions::new(
|
||||
std::env::current_exe()?,
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
)?,
|
||||
)));
|
||||
|
||||
let (first_socket, _peer_addr) = timeout(Duration::from_secs(5), listener.accept()).await??;
|
||||
let mut first_websocket = accept_async(first_socket).await?;
|
||||
first_websocket.close(None).await?;
|
||||
|
||||
// An ordinary disconnect retries the same URL without registering again.
|
||||
let (mut rejected_socket, _peer_addr) =
|
||||
// An ordinary disconnect and a stalled upgrade both retry without registering again.
|
||||
let (mut stalled_socket, _peer_addr) =
|
||||
timeout(Duration::from_secs(5), listener.accept()).await??;
|
||||
let mut request = [0u8; 4096];
|
||||
assert!(stalled_socket.read(&mut request).await? > 0);
|
||||
tokio::time::pause();
|
||||
tokio::time::advance(DEFAULT_REMOTE_EXEC_SERVER_CONNECT_TIMEOUT).await;
|
||||
tokio::time::resume();
|
||||
|
||||
// Keep the stalled socket open until the executor starts its next attempt.
|
||||
let (mut rejected_socket, _peer_addr) =
|
||||
timeout(Duration::from_secs(5), listener.accept()).await??;
|
||||
drop(stalled_socket);
|
||||
let _ = rejected_socket.read(&mut request).await?;
|
||||
rejected_socket
|
||||
.write_all(b"HTTP/1.1 401 Unauthorized\r\nContent-Length: 0\r\n\r\n")
|
||||
@@ -209,8 +219,8 @@ async fn reconnect_reuses_registration_until_url_is_rejected() -> Result<()> {
|
||||
rejected_socket.shutdown().await?;
|
||||
|
||||
// The 4xx response discards the old registration before this attempt.
|
||||
let (third_socket, _peer_addr) = timeout(Duration::from_secs(5), listener.accept()).await??;
|
||||
let _third_websocket = accept_async(third_socket).await?;
|
||||
let (final_socket, _peer_addr) = timeout(Duration::from_secs(5), listener.accept()).await??;
|
||||
let _final_websocket = accept_async(final_socket).await?;
|
||||
registry.verify().await;
|
||||
|
||||
environment_task.abort();
|
||||
|
||||
@@ -1,8 +1,37 @@
|
||||
//! Spread executor reconnects after a shared outage without exceeding the existing backoff cap.
|
||||
//! Spread executor reconnects and retain backoff until a connection has been stable.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
pub(super) fn reconnect_delay(backoff: Duration, random_sample: u64) -> Duration {
|
||||
const INITIAL_BACKOFF: Duration = Duration::from_secs(1);
|
||||
const MAX_BACKOFF: Duration = Duration::from_secs(30);
|
||||
// Longer than the first keepalive interval plus its pong timeout (30s + 60s).
|
||||
const STABLE_CONNECTION_DURATION: Duration = Duration::from_secs(120);
|
||||
|
||||
pub(super) struct ReconnectBackoff {
|
||||
delay: Duration,
|
||||
}
|
||||
|
||||
impl ReconnectBackoff {
|
||||
pub(super) fn new() -> Self {
|
||||
Self {
|
||||
delay: INITIAL_BACKOFF,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn connection_closed(&mut self, connected_for: Duration) {
|
||||
if connected_for >= STABLE_CONNECTION_DURATION {
|
||||
self.delay = INITIAL_BACKOFF;
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn next_delay(&mut self, random_sample: u64) -> Duration {
|
||||
let delay = reconnect_delay(self.delay, random_sample);
|
||||
self.delay = (self.delay * 2).min(MAX_BACKOFF);
|
||||
delay
|
||||
}
|
||||
}
|
||||
|
||||
fn reconnect_delay(backoff: Duration, random_sample: u64) -> Duration {
|
||||
let upper_ms = backoff.as_millis() as u64;
|
||||
let lower_ms = upper_ms / 2;
|
||||
Duration::from_millis(lower_ms + random_sample % (upper_ms - lower_ms + 1))
|
||||
|
||||
@@ -4,6 +4,8 @@ use std::time::Duration;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::ReconnectBackoff;
|
||||
use super::STABLE_CONNECTION_DURATION;
|
||||
use super::reconnect_delay;
|
||||
|
||||
#[test]
|
||||
@@ -23,3 +25,24 @@ fn reconnect_jitter_stays_within_each_backoff_window() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flapping_connections_retain_backoff_until_a_stable_session() {
|
||||
let mut backoff = ReconnectBackoff::new();
|
||||
for seconds in [1, 2, 4, 8, 16, 30, 30] {
|
||||
backoff.connection_closed(STABLE_CONNECTION_DURATION - Duration::from_millis(1));
|
||||
assert_eq!(
|
||||
backoff.next_delay(/*random_sample*/ 0),
|
||||
Duration::from_millis(seconds * 500)
|
||||
);
|
||||
}
|
||||
backoff.connection_closed(STABLE_CONNECTION_DURATION);
|
||||
assert_eq!(
|
||||
backoff.next_delay(/*random_sample*/ 0),
|
||||
Duration::from_millis(500)
|
||||
);
|
||||
assert_eq!(
|
||||
backoff.next_delay(/*random_sample*/ 0),
|
||||
Duration::from_secs(1)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user