From 00acf0a2c75c27efde9e7d93411850983a153d1f Mon Sep 17 00:00:00 2001 From: James Braid Date: Tue, 3 Nov 2026 22:44:07 -0700 Subject: [PATCH] net/stream: time the redial test against its own clock a_peer_that_hangs_up_is_redialled_at_the_retry_rate counted accepts in a fixed one-second wall-clock window and asserted 5 to 15 of them. When the host was loaded the backend thread could be descheduled for part of that window, so it dialled fewer times than the interval allows and the test failed with "3 connections in one second". Wait for six accepts instead and check the time between the first and the last. The backend can be late but cannot dial sooner than one retry after the previous dial, so the span has a floor that load does not move. The only slack needed is for the first accept being noticed late, which the test allows one whole interval. A client that redials without waiting makes the six in a few milliseconds and fails. Checked by dropping the wait after a hung-up session: the test fails with six connections in 49ms. 200 sequential runs of the fixed test pass. --- src/net/stream.rs | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/net/stream.rs b/src/net/stream.rs index b81bef1c7..7cab68072 100644 --- a/src/net/stream.rs +++ b/src/net/stream.rs @@ -593,20 +593,34 @@ mod tests { /// interval, not in a tight loop. #[test] fn a_peer_that_hangs_up_is_redialled_at_the_retry_rate() { + const DIALS: u32 = 6; + let retry = Duration::from_millis(100); let listener = TcpListener::bind("127.0.0.1:0").unwrap(); listener.set_nonblocking(true).unwrap(); - let mut r = rig(StreamEndpoint::Tcp(listener.local_addr().unwrap()), Duration::from_millis(100)); - let (mut accepted, end) = (0, Instant::now() + Duration::from_secs(1)); - while Instant::now() < end { + let mut r = rig(StreamEndpoint::Tcp(listener.local_addr().unwrap()), retry); + // Time the redials against the clock the test reads itself, not a + // fixed window: a loaded host can stall the backend for part of a + // window, but it cannot make the dials come closer together. + let (mut first, mut last, mut accepted) = (None::, Instant::now(), 0); + let deadline = Instant::now() + Duration::from_secs(30); + while accepted < DIALS { + assert!(Instant::now() < deadline, "only {accepted} connections in 30 seconds"); if listener.accept().is_ok() { + last = Instant::now(); + first.get_or_insert(last); accepted += 1; } else { std::thread::sleep(Duration::from_millis(2)); } } r.stop(); - // About ten in the second; a spinning client makes thousands. - assert!((5..=15).contains(&accepted), "{accepted} connections in one second"); + // Each dial waits out a full retry after the previous one. We see + // them late, never early, so only the first sighting can shrink the + // span; allow it one whole interval of lateness. A spinning client + // makes all six in a few milliseconds. + let span = last - first.unwrap(); + let least = retry * (DIALS - 2); + assert!(span >= least, "{DIALS} connections in {span:?}, expected at least {least:?}"); } /// Frames queued while no peer was there are not sent to the one that