From 6fcc9e2098f34cabab3e5fcfca23a55bd17ccc40 Mon Sep 17 00:00:00 2001 From: Leynos Date: Sun, 10 Aug 2025 11:12:30 +0100 Subject: [PATCH 1/7] Fix runtime readiness signalling --- src/server/config/tests.rs | 26 +++++++++++++++----------- src/server/runtime.rs | 13 ++++++++----- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/server/config/tests.rs b/src/server/config/tests.rs index 177930a1..27ac2ed6 100644 --- a/src/server/config/tests.rs +++ b/src/server/config/tests.rs @@ -25,6 +25,12 @@ use crate::server::test_util::{ server_with_preamble, }; +#[derive(Clone, Copy)] +enum HandlerKind { + Success, + Failure, +} + fn expected_default_worker_count() -> usize { // Mirror the default worker logic to keep tests aligned with `WireframeServer::new`. std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get) @@ -92,36 +98,34 @@ async fn test_local_addr_after_bind( } #[rstest] -#[case("success")] -#[case("failure")] +#[case(HandlerKind::Success)] +#[case(HandlerKind::Failure)] #[tokio::test] async fn test_preamble_handler_registration( factory: impl Fn() -> WireframeApp + Send + Sync + Clone + 'static, - #[case] handler_type: &str, + #[case] handler: HandlerKind, ) { let counter = Arc::new(AtomicUsize::new(0)); let c = counter.clone(); let server = server_with_preamble(factory); - let server = match handler_type { - "success" => server.on_preamble_decode_success(move |_p: &TestPreamble, _| { + let server = match handler { + HandlerKind::Success => server.on_preamble_decode_success(move |_p: &TestPreamble, _| { let c = c.clone(); Box::pin(async move { c.fetch_add(1, Ordering::SeqCst); Ok(()) }) }), - "failure" => server.on_preamble_decode_failure(move |_err: &DecodeError| { + HandlerKind::Failure => server.on_preamble_decode_failure(move |_err: &DecodeError| { c.fetch_add(1, Ordering::SeqCst); }), - _ => panic!("Invalid handler type"), }; assert_eq!(counter.load(Ordering::SeqCst), 0); - match handler_type { - "success" => assert!(server.on_preamble_success.is_some()), - "failure" => assert!(server.on_preamble_failure.is_some()), - _ => unreachable!(), + match handler { + HandlerKind::Success => assert!(server.on_preamble_success.is_some()), + HandlerKind::Failure => assert!(server.on_preamble_failure.is_some()), } } diff --git a/src/server/runtime.rs b/src/server/runtime.rs index d8b12230..13ba7f2d 100644 --- a/src/server/runtime.rs +++ b/src/server/runtime.rs @@ -130,12 +130,15 @@ where .. } = self; - if let Some(tx) = ready_tx - && tx.send(()).is_err() - { - tracing::warn!("Failed to send readiness signal: receiver dropped"); + #[expect(clippy::collapsible_if)] + if let Some(tx) = ready_tx { + if tx.send(()).is_err() { + tracing::warn!("Failed to send readiness signal: receiver dropped"); + } } + let backoff_config = BackoffConfig::default(); + let shutdown_token = CancellationToken::new(); let tracker = TaskTracker::new(); @@ -153,7 +156,7 @@ where on_failure, token, t, - BackoffConfig::default(), + backoff_config, )); } From 064734706df7f7fccb91f6fbf6b9ecb11945da32 Mon Sep 17 00:00:00 2001 From: Leynos Date: Sun, 10 Aug 2025 12:50:46 +0100 Subject: [PATCH 2/7] Store backoff config and tidy readiness send --- src/server/config/binding.rs | 2 ++ src/server/config/mod.rs | 3 ++- src/server/config/preamble.rs | 1 + src/server/config/tests.rs | 45 ++++++++++++++++++++++++++++------- src/server/mod.rs | 1 + src/server/runtime.rs | 10 +++----- 6 files changed, 45 insertions(+), 17 deletions(-) diff --git a/src/server/config/binding.rs b/src/server/config/binding.rs index fdadc041..23a2f011 100644 --- a/src/server/config/binding.rs +++ b/src/server/config/binding.rs @@ -89,6 +89,7 @@ where on_preamble_success: self.on_preamble_success, on_preamble_failure: self.on_preamble_failure, ready_tx: self.ready_tx, + backoff_config: self.backoff_config, state: Bound { listener: Arc::new(tokio), }, @@ -174,6 +175,7 @@ where on_preamble_success: self.on_preamble_success, on_preamble_failure: self.on_preamble_failure, ready_tx: self.ready_tx, + backoff_config: self.backoff_config, state: Bound { listener: Arc::new(tokio), }, diff --git a/src/server/config/mod.rs b/src/server/config/mod.rs index fa5ce2da..9f4c3eb1 100644 --- a/src/server/config/mod.rs +++ b/src/server/config/mod.rs @@ -12,7 +12,7 @@ use core::marker::PhantomData; use tokio::sync::oneshot; -use super::{ServerState, Unbound, WireframeServer}; +use super::{BackoffConfig, ServerState, Unbound, WireframeServer}; use crate::{app::WireframeApp, preamble::Preamble}; macro_rules! builder_setter { @@ -72,6 +72,7 @@ where on_preamble_success: None, on_preamble_failure: None, ready_tx: None, + backoff_config: BackoffConfig::default(), state: Unbound, _preamble: PhantomData, } diff --git a/src/server/config/preamble.rs b/src/server/config/preamble.rs index 308b8f1c..0a588374 100644 --- a/src/server/config/preamble.rs +++ b/src/server/config/preamble.rs @@ -46,6 +46,7 @@ where on_preamble_success: None, on_preamble_failure: None, ready_tx: self.ready_tx, + backoff_config: self.backoff_config, state: self.state, _preamble: PhantomData, } diff --git a/src/server/config/tests.rs b/src/server/config/tests.rs index 27ac2ed6..b9903610 100644 --- a/src/server/config/tests.rs +++ b/src/server/config/tests.rs @@ -17,6 +17,7 @@ use std::{ use rstest::rstest; use super::*; +use bincode::error::DecodeError; use crate::server::test_util::{ TestPreamble, bind_server, @@ -24,9 +25,10 @@ use crate::server::test_util::{ free_port, server_with_preamble, }; +use tokio::net::{TcpListener, TcpStream}; -#[derive(Clone, Copy)] -enum HandlerKind { +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum PreambleHandlerKind { Success, Failure, } @@ -98,35 +100,60 @@ async fn test_local_addr_after_bind( } #[rstest] -#[case(HandlerKind::Success)] -#[case(HandlerKind::Failure)] +#[case::success(PreambleHandlerKind::Success)] +#[case::failure(PreambleHandlerKind::Failure)] #[tokio::test] async fn test_preamble_handler_registration( factory: impl Fn() -> WireframeApp + Send + Sync + Clone + 'static, - #[case] handler: HandlerKind, + #[case] handler: PreambleHandlerKind, ) { let counter = Arc::new(AtomicUsize::new(0)); let c = counter.clone(); let server = server_with_preamble(factory); let server = match handler { - HandlerKind::Success => server.on_preamble_decode_success(move |_p: &TestPreamble, _| { + PreambleHandlerKind::Success => server.on_preamble_decode_success(move |_p: &TestPreamble, _| { let c = c.clone(); Box::pin(async move { c.fetch_add(1, Ordering::SeqCst); Ok(()) }) }), - HandlerKind::Failure => server.on_preamble_decode_failure(move |_err: &DecodeError| { + PreambleHandlerKind::Failure => server.on_preamble_decode_failure(move |_err: &DecodeError| { c.fetch_add(1, Ordering::SeqCst); }), }; assert_eq!(counter.load(Ordering::SeqCst), 0); match handler { - HandlerKind::Success => assert!(server.on_preamble_success.is_some()), - HandlerKind::Failure => assert!(server.on_preamble_failure.is_some()), + PreambleHandlerKind::Success => { + assert!(server.on_preamble_success.is_some()); + let handler = server + .on_preamble_success + .as_ref() + .expect("success handler missing"); + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("bind listener"); + let addr = listener.local_addr().expect("listener addr"); + let client = TcpStream::connect(addr); + let (mut stream, _) = listener.accept().await.expect("accept stream"); + let _ = client.await; + let preamble = TestPreamble { id: 0, message: String::new() }; + handler(&preamble, &mut stream) + .await + .expect("handler failed"); + } + PreambleHandlerKind::Failure => { + assert!(server.on_preamble_failure.is_some()); + let handler = server + .on_preamble_failure + .as_ref() + .expect("failure handler missing"); + handler(&DecodeError::UnexpectedEnd); + } } + assert_eq!(counter.load(Ordering::SeqCst), 1); } #[rstest] diff --git a/src/server/mod.rs b/src/server/mod.rs index 1177e77d..16524261 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -119,6 +119,7 @@ where /// Because only one notification may be sent, a new `ready_tx` must be /// provided each time the server is started. pub(crate) ready_tx: Option>, + pub(crate) backoff_config: BackoffConfig, /// Typestate tracking whether the server has been bound to a listener. /// [`Unbound`] servers require binding before they can run. pub(crate) state: S, diff --git a/src/server/runtime.rs b/src/server/runtime.rs index 13ba7f2d..b6949e11 100644 --- a/src/server/runtime.rs +++ b/src/server/runtime.rs @@ -127,18 +127,14 @@ where on_preamble_failure, ready_tx, state: Bound { listener }, + backoff_config, .. } = self; - #[expect(clippy::collapsible_if)] - if let Some(tx) = ready_tx { - if tx.send(()).is_err() { - tracing::warn!("Failed to send readiness signal: receiver dropped"); - } + if ready_tx.is_some_and(|tx| tx.send(()).is_err()) { + tracing::warn!("Failed to send readiness signal: receiver dropped"); } - let backoff_config = BackoffConfig::default(); - let shutdown_token = CancellationToken::new(); let tracker = TaskTracker::new(); From 8a22dff256c32a461a85ca6326895fc2d5f4bfe7 Mon Sep 17 00:00:00 2001 From: Leynos Date: Wed, 13 Aug 2025 18:41:02 +0100 Subject: [PATCH 3/7] Expose accept backoff builder and defer readiness signal --- docs/server/configuration.md | 15 +++++----- src/server/config/mod.rs | 9 ++++++ src/server/config/tests.rs | 58 +++++++++++++++++++++++------------- src/server/runtime.rs | 32 ++++++++++++++++---- 4 files changed, 81 insertions(+), 33 deletions(-) diff --git a/docs/server/configuration.md b/docs/server/configuration.md index c93582b2..a5da31e9 100644 --- a/docs/server/configuration.md +++ b/docs/server/configuration.md @@ -7,8 +7,7 @@ fails. ## Accept loop backoff The accept loop retries failed `accept()` calls using exponential backoff. -`accept_backoff(initial_delay, max_delay)` sets both bounds in one call. These -values are stored in `BackoffConfig`: +`accept_backoff(cfg)` sets both bounds using a [`BackoffConfig`] value: - `initial_delay` – starting delay for the first retry, clamped to at least 1 millisecond. @@ -24,11 +23,13 @@ values are stored in `BackoffConfig`: ```rust use std::time::Duration; -use wireframe::{app::WireframeApp, server::WireframeServer}; +use wireframe::{app::WireframeApp, server::{WireframeServer, BackoffConfig}}; + +let cfg = BackoffConfig { + initial_delay: Duration::from_millis(5), + max_delay: Duration::from_millis(500), +}; let server = WireframeServer::new(|| WireframeApp::default()) - .accept_backoff(Duration::from_millis(5), Duration::from_millis(500)); + .accept_backoff(cfg); ``` - -`accept_initial_delay` and `accept_max_delay` allow adjusting each parameter -individually. diff --git a/src/server/config/mod.rs b/src/server/config/mod.rs index 9f4c3eb1..ae1b903d 100644 --- a/src/server/config/mod.rs +++ b/src/server/config/mod.rs @@ -119,6 +119,15 @@ where ready_signal, ready_tx, tx: oneshot::Sender<()> => Some(tx) ); + builder_setter!( + /// Configure accept-loop backoff behaviour. + /// + /// Invariants: + /// - `initial_delay` must be >= 1 ms + /// - `initial_delay` must be <= `max_delay` + accept_backoff, backoff_config, cfg: BackoffConfig => cfg.normalised() + ); + /// Returns the configured number of worker tasks for the server. /// /// # Examples diff --git a/src/server/config/tests.rs b/src/server/config/tests.rs index b9903610..dc4fc304 100644 --- a/src/server/config/tests.rs +++ b/src/server/config/tests.rs @@ -18,12 +18,15 @@ use rstest::rstest; use super::*; use bincode::error::DecodeError; -use crate::server::test_util::{ - TestPreamble, - bind_server, - factory, - free_port, - server_with_preamble, +use crate::server::{ + test_util::{ + TestPreamble, + bind_server, + factory, + free_port, + server_with_preamble, + }, + BackoffConfig, }; use tokio::net::{TcpListener, TcpStream}; @@ -233,11 +236,12 @@ async fn test_bind_to_multiple_addresses( fn test_accept_backoff_configuration( factory: impl Fn() -> WireframeApp + Send + Sync + Clone + 'static, ) { - let initial = Duration::from_millis(5); - let max = Duration::from_millis(500); - let server = WireframeServer::new(factory).accept_backoff(initial, max); - assert_eq!(server.backoff_config.initial_delay, initial); - assert_eq!(server.backoff_config.max_delay, max); + let cfg = BackoffConfig { + initial_delay: Duration::from_millis(5), + max_delay: Duration::from_millis(500), + }; + let server = WireframeServer::new(factory).accept_backoff(cfg); + assert_eq!(server.backoff_config, cfg); } /// Behaviour test verifying exponential delay doubling and capping. @@ -293,7 +297,8 @@ fn test_accept_initial_delay_configuration( factory: impl Fn() -> WireframeApp + Send + Sync + Clone + 'static, ) { let delay = Duration::from_millis(20); - let server = WireframeServer::new(factory).accept_initial_delay(delay); + let cfg = BackoffConfig { initial_delay: delay, ..BackoffConfig::default() }; + let server = WireframeServer::new(factory).accept_backoff(cfg); assert_eq!(server.backoff_config.initial_delay, delay); } @@ -302,21 +307,25 @@ fn test_accept_max_delay_configuration( factory: impl Fn() -> WireframeApp + Send + Sync + Clone + 'static, ) { let delay = Duration::from_millis(2000); - let server = WireframeServer::new(factory).accept_max_delay(delay); + let cfg = BackoffConfig { max_delay: delay, ..BackoffConfig::default() }; + let server = WireframeServer::new(factory).accept_backoff(cfg); assert_eq!(server.backoff_config.max_delay, delay); } #[rstest] fn test_backoff_validation(factory: impl Fn() -> WireframeApp + Send + Sync + Clone + 'static) { - let server = WireframeServer::new(factory.clone()).accept_initial_delay(Duration::ZERO); + let server = WireframeServer::new(factory.clone()) + .accept_backoff(BackoffConfig { initial_delay: Duration::ZERO, ..BackoffConfig::default() }); assert_eq!( server.backoff_config.initial_delay, Duration::from_millis(1) ); let server = WireframeServer::new(factory) - .accept_initial_delay(Duration::from_millis(100)) - .accept_max_delay(Duration::from_millis(50)); + .accept_backoff(BackoffConfig { + initial_delay: Duration::from_millis(100), + max_delay: Duration::from_millis(50), + }); assert_eq!(server.backoff_config.max_delay, Duration::from_millis(100)); } @@ -334,7 +343,11 @@ fn test_backoff_default_values(factory: impl Fn() -> WireframeApp + Send + Sync fn test_initial_delay_exceeds_default_max( factory: impl Fn() -> WireframeApp + Send + Sync + Clone + 'static, ) { - let server = WireframeServer::new(factory).accept_initial_delay(Duration::from_secs(2)); + let cfg = BackoffConfig { + initial_delay: Duration::from_secs(2), + max_delay: Duration::from_secs(1), + }; + let server = WireframeServer::new(factory).accept_backoff(cfg); assert_eq!(server.backoff_config.initial_delay, Duration::from_secs(2)); assert_eq!(server.backoff_config.max_delay, Duration::from_secs(2)); } @@ -343,15 +356,20 @@ fn test_initial_delay_exceeds_default_max( fn test_accept_backoff_parameter_swapping( factory: impl Fn() -> WireframeApp + Send + Sync + Clone + 'static, ) { - let server = WireframeServer::new(factory.clone()) - .accept_backoff(Duration::from_millis(5), Duration::from_millis(1)); + let server = WireframeServer::new(factory.clone()).accept_backoff(BackoffConfig { + initial_delay: Duration::from_millis(5), + max_delay: Duration::from_millis(1), + }); assert_eq!( server.backoff_config.initial_delay, Duration::from_millis(1) ); assert_eq!(server.backoff_config.max_delay, Duration::from_millis(5)); - let server = WireframeServer::new(factory).accept_backoff(Duration::ZERO, Duration::ZERO); + let server = WireframeServer::new(factory).accept_backoff(BackoffConfig { + initial_delay: Duration::ZERO, + max_delay: Duration::ZERO, + }); assert_eq!( server.backoff_config.initial_delay, Duration::from_millis(1) diff --git a/src/server/runtime.rs b/src/server/runtime.rs index b6949e11..52837f4b 100644 --- a/src/server/runtime.rs +++ b/src/server/runtime.rs @@ -36,7 +36,7 @@ use crate::{app::WireframeApp, preamble::Preamble}; /// # Invariants /// - `initial_delay` must not exceed `max_delay` /// - `initial_delay` must be at least 1 millisecond -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct BackoffConfig { pub initial_delay: Duration, pub max_delay: Duration, @@ -51,6 +51,18 @@ impl Default for BackoffConfig { } } +impl BackoffConfig { + #[must_use] + pub fn normalised(mut self) -> Self { + self.initial_delay = self.initial_delay.max(Duration::from_millis(1)); + self.max_delay = self.max_delay.max(Duration::from_millis(1)); + if self.initial_delay > self.max_delay { + std::mem::swap(&mut self.initial_delay, &mut self.max_delay); + } + self + } +} + impl WireframeServer where F: Fn() -> WireframeApp + Send + Sync + Clone + 'static, @@ -130,11 +142,6 @@ where backoff_config, .. } = self; - - if ready_tx.is_some_and(|tx| tx.send(()).is_err()) { - tracing::warn!("Failed to send readiness signal: receiver dropped"); - } - let shutdown_token = CancellationToken::new(); let tracker = TaskTracker::new(); @@ -156,6 +163,11 @@ where )); } + // Signal readiness after all workers have been spawned. + if ready_tx.is_some_and(|tx| tx.send(()).is_err()) { + tracing::warn!("Failed to send readiness signal: receiver dropped"); + } + select! { () = shutdown => shutdown_token.cancel(), () = tracker.wait() => {}, @@ -179,6 +191,14 @@ pub(super) async fn accept_loop( F: Fn() -> WireframeApp + Send + Sync + Clone + 'static, T: Preamble, { + debug_assert!( + backoff_config.initial_delay <= backoff_config.max_delay, + "BackoffConfig invariant violated: initial_delay > max_delay" + ); + debug_assert!( + backoff_config.initial_delay >= Duration::from_millis(1), + "BackoffConfig invariant violated: initial_delay < 1ms" + ); let mut delay = backoff_config.initial_delay; loop { select! { From 0030b1931d51b69eada15270626b4ef524346ff1 Mon Sep 17 00:00:00 2001 From: Leynos Date: Thu, 14 Aug 2025 02:38:31 +0100 Subject: [PATCH 4/7] Clarify accept backoff docs and tests --- docs/server/configuration.md | 2 +- src/server/config/mod.rs | 2 ++ src/server/config/tests.rs | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/server/configuration.md b/docs/server/configuration.md index a5da31e9..bd16c5b5 100644 --- a/docs/server/configuration.md +++ b/docs/server/configuration.md @@ -7,7 +7,7 @@ fails. ## Accept loop backoff The accept loop retries failed `accept()` calls using exponential backoff. -`accept_backoff(cfg)` sets both bounds using a [`BackoffConfig`] value: +`accept_backoff(cfg)` sets both bounds using a `BackoffConfig` value: - `initial_delay` – starting delay for the first retry, clamped to at least 1 millisecond. diff --git a/src/server/config/mod.rs b/src/server/config/mod.rs index ae1b903d..04b01044 100644 --- a/src/server/config/mod.rs +++ b/src/server/config/mod.rs @@ -125,6 +125,8 @@ where /// Invariants: /// - `initial_delay` must be >= 1 ms /// - `initial_delay` must be <= `max_delay` + /// + /// The provided configuration is normalised before being stored. accept_backoff, backoff_config, cfg: BackoffConfig => cfg.normalised() ); diff --git a/src/server/config/tests.rs b/src/server/config/tests.rs index dc4fc304..eba0cff8 100644 --- a/src/server/config/tests.rs +++ b/src/server/config/tests.rs @@ -348,7 +348,7 @@ fn test_initial_delay_exceeds_default_max( max_delay: Duration::from_secs(1), }; let server = WireframeServer::new(factory).accept_backoff(cfg); - assert_eq!(server.backoff_config.initial_delay, Duration::from_secs(2)); + assert_eq!(server.backoff_config.initial_delay, Duration::from_secs(1)); assert_eq!(server.backoff_config.max_delay, Duration::from_secs(2)); } From 040e81edb22c7294f9e5156a0a93a64aeb0ae4c5 Mon Sep 17 00:00:00 2001 From: Leynos Date: Thu, 14 Aug 2025 13:08:16 +0100 Subject: [PATCH 5/7] Document accept backoff normalisation Explain that accept_backoff normalises BackoffConfig so out-of-range values are clamped or swapped before storage. --- docs/server/configuration.md | 4 +++- src/server/config/mod.rs | 9 +++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/server/configuration.md b/docs/server/configuration.md index bd16c5b5..d5a79cfd 100644 --- a/docs/server/configuration.md +++ b/docs/server/configuration.md @@ -7,7 +7,9 @@ fails. ## Accept loop backoff The accept loop retries failed `accept()` calls using exponential backoff. -`accept_backoff(cfg)` sets both bounds using a `BackoffConfig` value: +`accept_backoff(cfg)` sets both bounds using a `BackoffConfig` value. The +builder normalises the supplied configuration via `BackoffConfig::normalised`, +so out-of-range values are adjusted rather than preserved: - `initial_delay` – starting delay for the first retry, clamped to at least 1 millisecond. diff --git a/src/server/config/mod.rs b/src/server/config/mod.rs index 04b01044..b7a78066 100644 --- a/src/server/config/mod.rs +++ b/src/server/config/mod.rs @@ -122,11 +122,16 @@ where builder_setter!( /// Configure accept-loop backoff behaviour. /// + /// The supplied configuration is passed to + /// [`BackoffConfig::normalised`] (`cfg.normalised()`) before being + /// stored. Normalisation clamps `initial_delay` to at least 1 ms and no + /// greater than `max_delay`, applying any other adjustments + /// `BackoffConfig::normalised` defines so out-of-range values are + /// corrected rather than preserved. + /// /// Invariants: /// - `initial_delay` must be >= 1 ms /// - `initial_delay` must be <= `max_delay` - /// - /// The provided configuration is normalised before being stored. accept_backoff, backoff_config, cfg: BackoffConfig => cfg.normalised() ); From b3c9173384c3a6a90bc116f73989597f8d2c5e8b Mon Sep 17 00:00:00 2001 From: Leynos Date: Thu, 14 Aug 2025 17:25:21 +0100 Subject: [PATCH 6/7] Clarify backoff normalisation and assert client connections --- src/server/config/mod.rs | 3 ++- src/server/config/tests.rs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/server/config/mod.rs b/src/server/config/mod.rs index b7a78066..2a87ba50 100644 --- a/src/server/config/mod.rs +++ b/src/server/config/mod.rs @@ -125,7 +125,8 @@ where /// The supplied configuration is passed to /// [`BackoffConfig::normalised`] (`cfg.normalised()`) before being /// stored. Normalisation clamps `initial_delay` to at least 1 ms and no - /// greater than `max_delay`, applying any other adjustments + /// greater than `max_delay`. If `initial_delay` exceeds `max_delay`, + /// the values are swapped. Normalisation applies any other adjustments /// `BackoffConfig::normalised` defines so out-of-range values are /// corrected rather than preserved. /// diff --git a/src/server/config/tests.rs b/src/server/config/tests.rs index eba0cff8..1dc44bb7 100644 --- a/src/server/config/tests.rs +++ b/src/server/config/tests.rs @@ -141,7 +141,7 @@ async fn test_preamble_handler_registration( let addr = listener.local_addr().expect("listener addr"); let client = TcpStream::connect(addr); let (mut stream, _) = listener.accept().await.expect("accept stream"); - let _ = client.await; + client.await.expect("client connect failed"); let preamble = TestPreamble { id: 0, message: String::new() }; handler(&preamble, &mut stream) .await From 255670978de61bd8950dc58ac6fba17910d302e2 Mon Sep 17 00:00:00 2001 From: Leynos Date: Thu, 14 Aug 2025 19:50:56 +0100 Subject: [PATCH 7/7] Prevent deadlock in preamble handler test Await the client connection before calling accept to avoid hanging in the preamble handler test. Also assert the swapped initial delay in backoff validation. --- src/server/config/tests.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/server/config/tests.rs b/src/server/config/tests.rs index 1dc44bb7..237ea0ef 100644 --- a/src/server/config/tests.rs +++ b/src/server/config/tests.rs @@ -139,9 +139,10 @@ async fn test_preamble_handler_registration( .await .expect("bind listener"); let addr = listener.local_addr().expect("listener addr"); - let client = TcpStream::connect(addr); + let _client = TcpStream::connect(addr) + .await + .expect("client connect failed"); let (mut stream, _) = listener.accept().await.expect("accept stream"); - client.await.expect("client connect failed"); let preamble = TestPreamble { id: 0, message: String::new() }; handler(&preamble, &mut stream) .await @@ -326,6 +327,10 @@ fn test_backoff_validation(factory: impl Fn() -> WireframeApp + Send + Sync + Cl initial_delay: Duration::from_millis(100), max_delay: Duration::from_millis(50), }); + assert_eq!( + server.backoff_config.initial_delay, + Duration::from_millis(50) + ); assert_eq!(server.backoff_config.max_delay, Duration::from_millis(100)); }