From cec4747168e89942976120e7b44699d8fa5af20e Mon Sep 17 00:00:00 2001 From: zhc Date: Wed, 1 Jul 2026 03:00:20 +0000 Subject: [PATCH 1/7] feat(rwnd): add rwnd trace and its unit test --- Cargo.toml | 2 + src/lib.rs | 61 ++++++ src/model/mod.rs | 13 +- src/model/rwnd.rs | 538 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 612 insertions(+), 2 deletions(-) create mode 100644 src/model/rwnd.rs diff --git a/Cargo.toml b/Cargo.toml index 33f5978..bdd7957 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,12 +43,14 @@ model = [ "delay-per-packet-model", "loss-model", "duplicate-model", + "rwnd-model", ] bw-model = ["dep:rand", "dep:rand_distr", "dep:dyn-clone"] delay-model = ["dep:dyn-clone"] delay-per-packet-model = ["dep:dyn-clone"] loss-model = ["dep:dyn-clone"] duplicate-model = ["dep:dyn-clone"] +rwnd-model = ["dep:dyn-clone"] serde = ["dep:serde", "dep:typetag", "bandwidth/serde"] mahimahi = ["dep:itertools"] human = [ diff --git a/src/lib.rs b/src/lib.rs index bbdc68b..2f18417 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -113,6 +113,7 @@ pub use mahimahi::{load_mahimahi_trace, Mahimahi, MahimahiExt}; feature = "delay-model", feature = "loss-model", feature = "duplicate-model", + feature = "rwnd-model", feature = "model", ))] pub mod model; @@ -126,6 +127,14 @@ pub use std::time::Duration; /// The delay describes how long a packet is delayed when going through. pub type Delay = std::time::Duration; +/// A receive window value in bytes. +/// +/// Used for `set_rcv_buf`, `app_read_bytes`, and `rwnd_remaining` fields in +/// [`RwndDecision`]. All three are byte counts even though they describe +/// different things (a configured buffer size, a consumed amount, an observed +/// remaining window), so they share a single integer type. +pub type Rwnd = u64; + /// The loss_pattern describes how the packets are dropped when going through. /// /// The loss_pattern is a sequence of conditional probabilities describing how packets are dropped. @@ -237,6 +246,58 @@ pub trait DuplicateTrace: Send { fn next_duplicate(&mut self) -> Option<(DuplicatePattern, Duration)>; } +/// The action a rwnd trace instructs the receiver to take at a single step. +/// +/// Exactly one variant is present per step — the type-level encoding of the +/// "exactly one of `app_read_bytes` / `rwnd_remaining`" rule. +/// +/// - `AppRead` drives the receiver model by simulating the application reading +/// `bytes` from the receive buffer; the resulting rwnd is computed from the +/// buffer state. +/// - `Remaining` skips the simulation and directly enforces an observed rwnd +/// of `rwnd` bytes — useful for replaying captured traces where only the +/// advertised window is known. +#[derive(Debug, Clone, PartialEq)] +pub enum RwndAction { + /// The simulated application reads this many bytes from the receive buffer at this step. + AppRead { bytes: u64 }, + /// The remaining rwnd value observed immediately after the app consumes data at this step. + Remaining { rwnd: u64 }, +} + +/// A single receive-side decision emitted by a [`RwndTrace`]. +/// +/// Each step of a rwnd trace produces one `RwndDecision` paired with a +/// [`Duration`] (see [`RwndTrace`]). The `set_rcv_buf` field is optional and +/// independent of [`RwndAction`]: a step may resize the socket's receive +/// buffer at the same time it advances the app-read or observed-remaining state. +#[derive(Debug, Clone, PartialEq)] +pub struct RwndDecision { + /// If `Some`, reconfigure the socket's receive buffer to this size at this step. + pub set_rcv_buf: Option, + /// The app-read or observed-remaining action for this step. + pub action: RwndAction, +} + +/// This is a trait that represents a trace of receive-window decisions over time. +/// +/// The trace is a sequence of `(rwnd_decision, duration)` pairs. The decision +/// describes how the socket's receive buffer, the application's read behavior, +/// and/or the observed remaining window change at this step; the duration is +/// how long this configuration lasts before the next step applies. +/// +/// For example, if the sequence is +/// `[(set_rcv_buf=64KB, app_read=1KB, 1s), (rwnd_remaining=32KB, 2s)]`, +/// then the receive buffer is resized to 64KB and the app reads 1KB for 1s, +/// then the observed rwnd becomes 32KB for 2s. +/// +/// Each `next_rwnd` call returns **the next decision and its duration** in the +/// sequence, or **None** when the trace is exhausted. Mirrors the shape of +/// [`BwTrace`], [`DelayTrace`], and [`LossTrace`]. +pub trait RwndTrace: Send { + fn next_rwnd(&mut self) -> Option<(RwndDecision, Duration)>; +} + #[cfg(test)] mod test { use model::TraceBwConfig; diff --git a/src/model/mod.rs b/src/model/mod.rs index d94df95..b6a8e90 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -1,15 +1,16 @@ -//! This module contains pre-defined models for BwTrace, DelayTrace, LossTrace and DuplicateTrace. +//! This module contains pre-defined models for BwTrace, DelayTrace, LossTrace, DuplicateTrace and RwndTrace. //! //! A model has two parts: a configuration struct and a model struct. //! The configuration struct is used to configure the model and //! used for serialization/deserialization if `serde` feature is enabled. -//! The model struct which implements trait `BwTrace`, `DelayTrace`, `LossTrace` or `DuplicateTrace` +//! The model struct which implements trait `BwTrace`, `DelayTrace`, `LossTrace`, `DuplicateTrace` or `RwndTrace` //! is used to generate the trace and maintain inner states. //! //! Enable `bw-model` feature to use the BwTrace models. //! Enable `delay-model` feature to use the DelayTrace models. //! Enable `loss-model` feature to use the LossTrace models. //! Enable `duplicate-model` feature to use the DuplicateTrace models. +//! Enable `rwnd-model` feature to use the RwndTrace models. #[cfg(feature = "bw-model")] pub mod bw; @@ -60,5 +61,13 @@ pub use duplicate::{DuplicateTraceConfig, RepeatedDuplicatePatternConfig, Static #[cfg(feature = "duplicate-model")] pub use duplicate::{RepeatedDuplicatePattern, StaticDuplicate}; +#[cfg(feature = "rwnd-model")] +pub mod rwnd; + +#[cfg(feature = "rwnd-model")] +pub use rwnd::{RepeatedRwndPattern, StaticRwnd}; +#[cfg(feature = "rwnd-model")] +pub use rwnd::{RepeatedRwndPatternConfig, RwndActionConfig, RwndTraceConfig, StaticRwndConfig}; + #[cfg(feature = "truncated-normal")] pub mod solve_truncate; diff --git a/src/model/rwnd.rs b/src/model/rwnd.rs new file mode 100644 index 0000000..1b2404a --- /dev/null +++ b/src/model/rwnd.rs @@ -0,0 +1,538 @@ +//! This module contains some predefined rwnd trace models. +//! +//! Enabled with feature `rwnd-model` or `model`. +//! +//! ## Predefined models +//! +//! - [`StaticRwnd`]: A trace model with a single rwnd decision. +//! - [`RepeatedRwndPattern`]: A trace model with a repeated rwnd pattern. +//! +//! ## Examples +//! +//! An example to build model from configuration: +//! +//! ``` +//! # use netem_trace::model::StaticRwndConfig; +//! # use netem_trace::{Duration, RwndTrace, RwndAction}; +//! let mut static_rwnd = StaticRwndConfig::new() +//! .set_rcv_buf(65536) +//! .app_read(1024) +//! .duration(Duration::from_secs(1)) +//! .build(); +//! let (decision, duration) = static_rwnd.next_rwnd().unwrap(); +//! assert_eq!(decision.set_rcv_buf, Some(65536)); +//! assert_eq!(decision.action, RwndAction::AppRead { bytes: 1024 }); +//! assert_eq!(duration, Duration::from_secs(1)); +//! assert_eq!(static_rwnd.next_rwnd(), None); +//! ``` +//! +//! A more common use case is to build model from a configuration file (e.g. json file): +//! +//! ``` +//! # use netem_trace::model::{StaticRwndConfig, RwndTraceConfig}; +//! # use netem_trace::{Duration, RwndTrace, RwndAction}; +//! # #[cfg(feature = "human")] +//! # let config_file_content = "{\"RepeatedRwndPatternConfig\":{\"pattern\":[{\"StaticRwndConfig\":{\"duration\":\"1s\",\"set_rcv_buf\":65536,\"app_read_bytes\":1024}},{\"StaticRwndConfig\":{\"duration\":\"1s\",\"rwnd_remaining\":32768}}],\"count\":2}}"; +//! // The content would be "{\"RepeatedRwndPatternConfig\":{\"pattern\":[{\"StaticRwndConfig\":{\"duration\":{\"secs\":1,\"nanos\":0},\"set_rcv_buf\":65536,\"app_read_bytes\":1024}},{\"StaticRwndConfig\":{\"duration\":{\"secs\":1,\"nanos\":0},\"rwnd_remaining\":32768}}],\"count\":2}}" +//! // if the `human` feature is not enabled. +//! # #[cfg(not(feature = "human"))] +//! let config_file_content = "{\"RepeatedRwndPatternConfig\":{\"pattern\":[{\"StaticRwndConfig\":{\"duration\":{\"secs\":1,\"nanos\":0},\"set_rcv_buf\":65536,\"app_read_bytes\":1024}},{\"StaticRwndConfig\":{\"duration\":{\"secs\":1,\"nanos\":0},\"rwnd_remaining\":32768}}],\"count\":2}}"; +//! let des: Box = serde_json::from_str(config_file_content).unwrap(); +//! let mut model = des.into_model(); +//! let (decision, _) = model.next_rwnd().unwrap(); +//! assert_eq!(decision.action, RwndAction::AppRead { bytes: 1024 }); +//! let (decision, _) = model.next_rwnd().unwrap(); +//! assert_eq!(decision.action, RwndAction::Remaining { rwnd: 32768 }); +//! let (decision, _) = model.next_rwnd().unwrap(); +//! assert_eq!(decision.action, RwndAction::AppRead { bytes: 1024 }); +//! let (decision, _) = model.next_rwnd().unwrap(); +//! assert_eq!(decision.action, RwndAction::Remaining { rwnd: 32768 }); +//! assert_eq!(model.next_rwnd(), None); +//! ``` +//! +//! Each step must set **exactly one** of `app_read_bytes` or `rwnd_remaining` — +//! never both, never neither. A step that violates this rule fails to deserialize +//! with a clear error message; a programmatically-built config that violates it +//! panics in [`StaticRwndConfig::build`]. +use crate::{Duration, Rwnd, RwndAction, RwndDecision, RwndTrace}; +use dyn_clone::DynClone; + +/// This trait is used to convert a rwnd trace configuration into a rwnd trace model. +/// +/// Since trace model is often configured with files and often has inner states which +/// is not suitable to be serialized/deserialized, this trait makes it possible to +/// separate the configuration part into a simple struct for serialization/deserialization, and +/// construct the model from the configuration. +#[cfg_attr(feature = "serde", typetag::serde)] +pub trait RwndTraceConfig: DynClone + Send { + fn into_model(self: Box) -> Box; +} + +dyn_clone::clone_trait_object!(RwndTraceConfig); + +#[cfg(feature = "serde")] +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +/// The config-layer representation of the action at a rwnd step. +/// +/// This enum is the deserialized form of the mutually-exclusive +/// `app_read_bytes` / `rwnd_remaining` pair. [`StaticRwndConfig`]'s custom +/// serde impls flatten the active variant into the top level of the JSON +/// object, so this enum's own externally-tagged shape is rarely seen by users +/// — but it's serialized/deserialized on its own when used outside the +/// custom container impl. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq)] +pub enum RwndActionConfig { + AppRead { app_read_bytes: Rwnd }, + Remaining { rwnd_remaining: Rwnd }, +} + +/// The model of a static rwnd trace: a single decision valid for one duration. +/// +/// ## Examples +/// +/// ``` +/// # use netem_trace::model::StaticRwndConfig; +/// # use netem_trace::{Duration, RwndTrace, RwndAction}; +/// let mut static_rwnd = StaticRwndConfig::new() +/// .set_rcv_buf(65536) +/// .app_read(1024) +/// .duration(Duration::from_secs(1)) +/// .build(); +/// let (decision, duration) = static_rwnd.next_rwnd().unwrap(); +/// assert_eq!(decision.set_rcv_buf, Some(65536)); +/// assert_eq!(decision.action, RwndAction::AppRead { bytes: 1024 }); +/// assert_eq!(duration, Duration::from_secs(1)); +/// assert_eq!(static_rwnd.next_rwnd(), None); +/// ``` +#[derive(Debug, Clone)] +pub struct StaticRwnd { + pub decision: RwndDecision, + pub duration: Option, +} + +/// The configuration struct for [`StaticRwnd`]. +/// +/// The serialized JSON form is **flat** — the active variant of [`RwndActionConfig`] +/// is hoisted to the top level, so a step looks like +/// `{"duration":"1s","set_rcv_buf":65536,"app_read_bytes":1024}` (or +/// `{"duration":"1s","rwnd_remaining":32768}`), never with an `action` wrapper. +/// +/// Exactly one of `app_read_bytes` / `rwnd_remaining` must be set; the deserializer +/// rejects both-set and neither-set inputs. +#[derive(Debug, Clone, Default)] +pub struct StaticRwndConfig { + pub duration: Option, + pub set_rcv_buf: Option, + // None only when constructed via `new()`/`Default` and not yet configured; + // `build()` panics on None as a defense for programmatic construction. + pub action: Option, +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for StaticRwndConfig { + fn deserialize>(deserializer: D) -> Result { + #[derive(Deserialize, Default)] + #[serde(default)] + struct Helper { + #[cfg_attr(feature = "human", serde(with = "humantime_serde"))] + #[serde(default)] + duration: Option, + #[serde(default)] + set_rcv_buf: Option, + #[serde(default)] + app_read_bytes: Option, + #[serde(default)] + rwnd_remaining: Option, + } + + let h = Helper::deserialize(deserializer)?; + let action = match (h.app_read_bytes, h.rwnd_remaining) { + (Some(bytes), None) => RwndActionConfig::AppRead { + app_read_bytes: bytes, + }, + (None, Some(rwnd)) => RwndActionConfig::Remaining { + rwnd_remaining: rwnd, + }, + (Some(_), Some(_)) => { + return Err(serde::de::Error::custom( + "rwnd step cannot set both `app_read_bytes` and `rwnd_remaining`", + )); + } + (None, None) => { + return Err(serde::de::Error::custom( + "rwnd step must set exactly one of `app_read_bytes` or `rwnd_remaining`", + )); + } + }; + Ok(Self { + duration: h.duration, + set_rcv_buf: h.set_rcv_buf, + action: Some(action), + }) + } +} + +#[cfg(feature = "serde")] +impl Serialize for StaticRwndConfig { + fn serialize(&self, serializer: S) -> Result { + #[derive(Serialize)] + struct Out { + #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "human", serde(with = "humantime_serde"))] + duration: Option, + #[serde(skip_serializing_if = "Option::is_none")] + set_rcv_buf: Option, + #[serde(skip_serializing_if = "Option::is_none")] + app_read_bytes: Option, + #[serde(skip_serializing_if = "Option::is_none")] + rwnd_remaining: Option, + } + + let (app_read_bytes, rwnd_remaining) = match &self.action { + Some(RwndActionConfig::AppRead { app_read_bytes }) => (Some(*app_read_bytes), None), + Some(RwndActionConfig::Remaining { rwnd_remaining }) => (None, Some(*rwnd_remaining)), + None => (None, None), + }; + Out { + duration: self.duration, + set_rcv_buf: self.set_rcv_buf, + app_read_bytes, + rwnd_remaining, + } + .serialize(serializer) + } +} + +/// The model contains an array of rwnd trace models. +/// +/// Combine multiple rwnd trace models into one rwnd pattern, +/// and repeat the pattern for `count` times. +/// +/// If `count` is 0, the pattern will be repeated forever. +/// +/// ## Examples +/// +/// The most common use case is to read from a configuration file and +/// deserialize it into a [`RepeatedRwndPatternConfig`]: +/// +/// ``` +/// # use netem_trace::model::{StaticRwndConfig, RwndTraceConfig}; +/// # use netem_trace::{Duration, RwndTrace, RwndAction}; +/// # #[cfg(feature = "human")] +/// # let config_file_content = "{\"RepeatedRwndPatternConfig\":{\"pattern\":[{\"StaticRwndConfig\":{\"duration\":\"1s\",\"set_rcv_buf\":65536,\"app_read_bytes\":1024}},{\"StaticRwndConfig\":{\"duration\":\"1s\",\"rwnd_remaining\":32768}}],\"count\":2}}"; +/// # #[cfg(not(feature = "human"))] +/// let config_file_content = "{\"RepeatedRwndPatternConfig\":{\"pattern\":[{\"StaticRwndConfig\":{\"duration\":{\"secs\":1,\"nanos\":0},\"set_rcv_buf\":65536,\"app_read_bytes\":1024}},{\"StaticRwndConfig\":{\"duration\":{\"secs\":1,\"nanos\":0},\"rwnd_remaining\":32768}}],\"count\":2}}"; +/// let des: Box = serde_json::from_str(config_file_content).unwrap(); +/// let mut model = des.into_model(); +/// let (decision, _) = model.next_rwnd().unwrap(); +/// assert_eq!(decision.action, RwndAction::AppRead { bytes: 1024 }); +/// ``` +pub struct RepeatedRwndPattern { + pub pattern: Vec>, + pub count: usize, + current_model: Option>, + current_cycle: usize, + current_pattern: usize, +} + +/// The configuration struct for [`RepeatedRwndPattern`]. +/// +/// See [`RepeatedRwndPattern`] for more details. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(default))] +#[derive(Default, Clone)] +pub struct RepeatedRwndPatternConfig { + pub pattern: Vec>, + pub count: usize, +} + +impl RwndTrace for StaticRwnd { + fn next_rwnd(&mut self) -> Option<(RwndDecision, Duration)> { + if let Some(duration) = self.duration.take() { + if duration.is_zero() { + None + } else { + Some((self.decision.clone(), duration)) + } + } else { + None + } + } +} + +impl RwndTrace for RepeatedRwndPattern { + fn next_rwnd(&mut self) -> Option<(RwndDecision, Duration)> { + if self.pattern.is_empty() || (self.count != 0 && self.current_cycle >= self.count) { + None + } else { + if self.current_model.is_none() { + self.current_model = Some(self.pattern[self.current_pattern].clone().into_model()); + } + match self.current_model.as_mut().unwrap().next_rwnd() { + Some(rwnd) => Some(rwnd), + None => { + self.current_model = None; + self.current_pattern += 1; + if self.current_pattern >= self.pattern.len() { + self.current_pattern = 0; + self.current_cycle += 1; + if self.count != 0 && self.current_cycle >= self.count { + return None; + } + } + self.next_rwnd() + } + } + } + } +} + +impl StaticRwndConfig { + pub fn new() -> Self { + Self { + duration: None, + set_rcv_buf: None, + action: None, + } + } + + pub fn duration(mut self, duration: Duration) -> Self { + self.duration = Some(duration); + self + } + + pub fn set_rcv_buf(mut self, set_rcv_buf: Rwnd) -> Self { + self.set_rcv_buf = Some(set_rcv_buf); + self + } + + pub fn app_read(mut self, bytes: Rwnd) -> Self { + self.action = Some(RwndActionConfig::AppRead { + app_read_bytes: bytes, + }); + self + } + + pub fn remaining(mut self, rwnd: Rwnd) -> Self { + self.action = Some(RwndActionConfig::Remaining { + rwnd_remaining: rwnd, + }); + self + } + + pub fn build(self) -> StaticRwnd { + let action_cfg = self.action.expect( + "StaticRwndConfig::build called without setting one of `app_read_bytes` or `rwnd_remaining`", + ); + let action = match action_cfg { + RwndActionConfig::AppRead { app_read_bytes } => RwndAction::AppRead { + bytes: app_read_bytes, + }, + RwndActionConfig::Remaining { rwnd_remaining } => RwndAction::Remaining { + rwnd: rwnd_remaining, + }, + }; + StaticRwnd { + decision: RwndDecision { + set_rcv_buf: self.set_rcv_buf, + action, + }, + duration: Some(self.duration.unwrap_or_else(|| Duration::from_secs(1))), + } + } +} + +impl RepeatedRwndPatternConfig { + pub fn new() -> Self { + Self { + pattern: vec![], + count: 0, + } + } + + pub fn pattern(mut self, pattern: Vec>) -> Self { + self.pattern = pattern; + self + } + + pub fn count(mut self, count: usize) -> Self { + self.count = count; + self + } + + pub fn build(self) -> RepeatedRwndPattern { + RepeatedRwndPattern { + pattern: self.pattern, + count: self.count, + current_model: None, + current_cycle: 0, + current_pattern: 0, + } + } +} + +macro_rules! impl_rwnd_trace_config { + ($name:ident) => { + #[cfg_attr(feature = "serde", typetag::serde)] + impl RwndTraceConfig for $name { + fn into_model(self: Box<$name>) -> Box { + Box::new(self.build()) + } + } + }; +} + +impl_rwnd_trace_config!(StaticRwndConfig); +impl_rwnd_trace_config!(RepeatedRwndPatternConfig); + +#[cfg(test)] +mod test { + use super::*; + use crate::model::StaticRwndConfig; + use crate::RwndTrace; + + #[test] + fn test_static_rwnd_model_app_read() { + let mut static_rwnd = StaticRwndConfig::new() + .set_rcv_buf(65536) + .app_read(1024) + .duration(Duration::from_secs(1)) + .build(); + let (decision, duration) = static_rwnd.next_rwnd().unwrap(); + assert_eq!(decision.set_rcv_buf, Some(65536)); + assert_eq!(decision.action, RwndAction::AppRead { bytes: 1024 }); + assert_eq!(duration, Duration::from_secs(1)); + assert_eq!(static_rwnd.next_rwnd(), None); + } + + #[test] + fn test_static_rwnd_model_remaining() { + let mut static_rwnd = StaticRwndConfig::new() + .remaining(32768) + .duration(Duration::from_secs(2)) + .build(); + let (decision, duration) = static_rwnd.next_rwnd().unwrap(); + assert_eq!(decision.set_rcv_buf, None); + assert_eq!(decision.action, RwndAction::Remaining { rwnd: 32768 }); + assert_eq!(duration, Duration::from_secs(2)); + assert_eq!(static_rwnd.next_rwnd(), None); + } + + #[test] + fn test_repeated_rwnd_pattern() { + let pat = vec![ + Box::new( + StaticRwndConfig::new() + .app_read(1024) + .duration(Duration::from_secs(1)), + ) as Box, + Box::new( + StaticRwndConfig::new() + .remaining(32768) + .duration(Duration::from_secs(1)), + ) as Box, + ]; + let mut model = RepeatedRwndPatternConfig::new() + .pattern(pat) + .count(2) + .build(); + let next = model.next_rwnd().unwrap(); + assert_eq!(next.0.action, RwndAction::AppRead { bytes: 1024 }); + assert_eq!(next.1, Duration::from_secs(1)); + let next = model.next_rwnd().unwrap(); + assert_eq!(next.0.action, RwndAction::Remaining { rwnd: 32768 }); + let next = model.next_rwnd().unwrap(); + assert_eq!(next.0.action, RwndAction::AppRead { bytes: 1024 }); + let next = model.next_rwnd().unwrap(); + assert_eq!(next.0.action, RwndAction::Remaining { rwnd: 32768 }); + assert_eq!(model.next_rwnd(), None); + } + + #[test] + #[cfg(feature = "serde")] + fn test_serde_roundtrip_app_read() { + let cfg = Box::new( + StaticRwndConfig::new() + .set_rcv_buf(65536) + .app_read(1024) + .duration(Duration::from_secs(1)), + ) as Box; + let ser_str = serde_json::to_string(&cfg).unwrap(); + #[cfg(feature = "human")] + let expected = "{\"StaticRwndConfig\":{\"duration\":\"1s\",\"set_rcv_buf\":65536,\"app_read_bytes\":1024}}"; + #[cfg(not(feature = "human"))] + let expected = "{\"StaticRwndConfig\":{\"duration\":{\"secs\":1,\"nanos\":0},\"set_rcv_buf\":65536,\"app_read_bytes\":1024}}"; + assert_eq!(ser_str, expected); + + let des: Box = serde_json::from_str(&ser_str).unwrap(); + let mut model = des.into_model(); + let (decision, duration) = model.next_rwnd().unwrap(); + assert_eq!(decision.set_rcv_buf, Some(65536)); + assert_eq!(decision.action, RwndAction::AppRead { bytes: 1024 }); + assert_eq!(duration, Duration::from_secs(1)); + } + + #[test] + #[cfg(feature = "serde")] + fn test_serde_roundtrip_remaining() { + let cfg = Box::new( + StaticRwndConfig::new() + .remaining(32768) + .duration(Duration::from_secs(1)), + ) as Box; + let ser_str = serde_json::to_string(&cfg).unwrap(); + #[cfg(feature = "human")] + let expected = "{\"StaticRwndConfig\":{\"duration\":\"1s\",\"rwnd_remaining\":32768}}"; + #[cfg(not(feature = "human"))] + let expected = "{\"StaticRwndConfig\":{\"duration\":{\"secs\":1,\"nanos\":0},\"rwnd_remaining\":32768}}"; + assert_eq!(ser_str, expected); + + let des: Box = serde_json::from_str(&ser_str).unwrap(); + let mut model = des.into_model(); + let (decision, _) = model.next_rwnd().unwrap(); + assert_eq!(decision.action, RwndAction::Remaining { rwnd: 32768 }); + } + + #[test] + #[cfg(feature = "serde")] + fn test_serde_rejects_both() { + // Omit duration to avoid the human/non-human format ambiguity; we're testing + // the action constraint, not duration parsing. + let json = "{\"StaticRwndConfig\":{\"app_read_bytes\":1024,\"rwnd_remaining\":32768}}"; + let result: Result, _> = serde_json::from_str(json); + let err = result + .err() + .expect("deserialization should have failed") + .to_string(); + assert!( + err.contains("cannot set both"), + "expected 'cannot set both' in error, got: {err}" + ); + } + + #[test] + #[cfg(feature = "serde")] + fn test_serde_rejects_neither() { + // Omit duration to avoid the human/non-human format ambiguity; we're testing + // the action constraint, not duration parsing. + let json = "{\"StaticRwndConfig\":{\"set_rcv_buf\":65536}}"; + let result: Result, _> = serde_json::from_str(json); + let err = result + .err() + .expect("deserialization should have failed") + .to_string(); + assert!( + err.contains("exactly one"), + "expected 'exactly one' in error, got: {err}" + ); + } + + #[test] + #[should_panic( + expected = "StaticRwndConfig::build called without setting one of `app_read_bytes` or `rwnd_remaining`" + )] + fn test_build_panics_without_action() { + StaticRwndConfig::new().build(); + } +} From 04e0e9f93c882c3ff1f2298e2267849b2bcdc6ee Mon Sep 17 00:00:00 2001 From: zhc Date: Wed, 15 Jul 2026 09:22:46 +0000 Subject: [PATCH 2/7] refactor(rwnd): replace Rwnd type alias with u64 --- src/lib.rs | 8 -------- src/model/rwnd.rs | 26 +++++++++++++------------- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2f18417..635a65b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -127,14 +127,6 @@ pub use std::time::Duration; /// The delay describes how long a packet is delayed when going through. pub type Delay = std::time::Duration; -/// A receive window value in bytes. -/// -/// Used for `set_rcv_buf`, `app_read_bytes`, and `rwnd_remaining` fields in -/// [`RwndDecision`]. All three are byte counts even though they describe -/// different things (a configured buffer size, a consumed amount, an observed -/// remaining window), so they share a single integer type. -pub type Rwnd = u64; - /// The loss_pattern describes how the packets are dropped when going through. /// /// The loss_pattern is a sequence of conditional probabilities describing how packets are dropped. diff --git a/src/model/rwnd.rs b/src/model/rwnd.rs index 1b2404a..25789b8 100644 --- a/src/model/rwnd.rs +++ b/src/model/rwnd.rs @@ -54,7 +54,7 @@ //! never both, never neither. A step that violates this rule fails to deserialize //! with a clear error message; a programmatically-built config that violates it //! panics in [`StaticRwndConfig::build`]. -use crate::{Duration, Rwnd, RwndAction, RwndDecision, RwndTrace}; +use crate::{Duration, RwndAction, RwndDecision, RwndTrace}; use dyn_clone::DynClone; /// This trait is used to convert a rwnd trace configuration into a rwnd trace model. @@ -84,8 +84,8 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq)] pub enum RwndActionConfig { - AppRead { app_read_bytes: Rwnd }, - Remaining { rwnd_remaining: Rwnd }, + AppRead { app_read_bytes: u64 }, + Remaining { rwnd_remaining: u64 }, } /// The model of a static rwnd trace: a single decision valid for one duration. @@ -124,7 +124,7 @@ pub struct StaticRwnd { #[derive(Debug, Clone, Default)] pub struct StaticRwndConfig { pub duration: Option, - pub set_rcv_buf: Option, + pub set_rcv_buf: Option, // None only when constructed via `new()`/`Default` and not yet configured; // `build()` panics on None as a defense for programmatic construction. pub action: Option, @@ -140,11 +140,11 @@ impl<'de> Deserialize<'de> for StaticRwndConfig { #[serde(default)] duration: Option, #[serde(default)] - set_rcv_buf: Option, + set_rcv_buf: Option, #[serde(default)] - app_read_bytes: Option, + app_read_bytes: Option, #[serde(default)] - rwnd_remaining: Option, + rwnd_remaining: Option, } let h = Helper::deserialize(deserializer)?; @@ -183,11 +183,11 @@ impl Serialize for StaticRwndConfig { #[cfg_attr(feature = "human", serde(with = "humantime_serde"))] duration: Option, #[serde(skip_serializing_if = "Option::is_none")] - set_rcv_buf: Option, + set_rcv_buf: Option, #[serde(skip_serializing_if = "Option::is_none")] - app_read_bytes: Option, + app_read_bytes: Option, #[serde(skip_serializing_if = "Option::is_none")] - rwnd_remaining: Option, + rwnd_remaining: Option, } let (app_read_bytes, rwnd_remaining) = match &self.action { @@ -302,19 +302,19 @@ impl StaticRwndConfig { self } - pub fn set_rcv_buf(mut self, set_rcv_buf: Rwnd) -> Self { + pub fn set_rcv_buf(mut self, set_rcv_buf: u64) -> Self { self.set_rcv_buf = Some(set_rcv_buf); self } - pub fn app_read(mut self, bytes: Rwnd) -> Self { + pub fn app_read(mut self, bytes: u64) -> Self { self.action = Some(RwndActionConfig::AppRead { app_read_bytes: bytes, }); self } - pub fn remaining(mut self, rwnd: Rwnd) -> Self { + pub fn remaining(mut self, rwnd: u64) -> Self { self.action = Some(RwndActionConfig::Remaining { rwnd_remaining: rwnd, }); From 574d1d6c1fb9971dbdd06e26ba2be395d8577237 Mon Sep 17 00:00:00 2001 From: zhc Date: Wed, 15 Jul 2026 09:33:34 +0000 Subject: [PATCH 3/7] refactor(rwnd): make RwndDecision action optional --- src/lib.rs | 15 ++++--- src/model/rwnd.rs | 112 ++++++++++++++++++++++------------------------ 2 files changed, 62 insertions(+), 65 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 635a65b..3775c28 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -240,8 +240,9 @@ pub trait DuplicateTrace: Send { /// The action a rwnd trace instructs the receiver to take at a single step. /// -/// Exactly one variant is present per step — the type-level encoding of the -/// "exactly one of `app_read_bytes` / `rwnd_remaining`" rule. +/// At most one action is present per step; a step that only reconfigures the +/// receive buffer (`set_rcv_buf`) without any read or observed-remaining update +/// leaves [`RwndDecision::action`] as `None`. /// /// - `AppRead` drives the receiver model by simulating the application reading /// `bytes` from the receive buffer; the resulting rwnd is computed from the @@ -260,15 +261,15 @@ pub enum RwndAction { /// A single receive-side decision emitted by a [`RwndTrace`]. /// /// Each step of a rwnd trace produces one `RwndDecision` paired with a -/// [`Duration`] (see [`RwndTrace`]). The `set_rcv_buf` field is optional and -/// independent of [`RwndAction`]: a step may resize the socket's receive -/// buffer at the same time it advances the app-read or observed-remaining state. +/// [`Duration`] (see [`RwndTrace`]). Both fields are optional and independent: +/// a step may resize the socket buffer, advance the receive model, both, or +/// neither (though a step that sets neither is effectively a no-op). #[derive(Debug, Clone, PartialEq)] pub struct RwndDecision { /// If `Some`, reconfigure the socket's receive buffer to this size at this step. pub set_rcv_buf: Option, - /// The app-read or observed-remaining action for this step. - pub action: RwndAction, + /// If `Some`, the app-read or observed-remaining action for this step. + pub action: Option, } /// This is a trait that represents a trace of receive-window decisions over time. diff --git a/src/model/rwnd.rs b/src/model/rwnd.rs index 25789b8..0c4abe1 100644 --- a/src/model/rwnd.rs +++ b/src/model/rwnd.rs @@ -21,7 +21,7 @@ //! .build(); //! let (decision, duration) = static_rwnd.next_rwnd().unwrap(); //! assert_eq!(decision.set_rcv_buf, Some(65536)); -//! assert_eq!(decision.action, RwndAction::AppRead { bytes: 1024 }); +//! assert_eq!(decision.action, Some(RwndAction::AppRead { bytes: 1024 })); //! assert_eq!(duration, Duration::from_secs(1)); //! assert_eq!(static_rwnd.next_rwnd(), None); //! ``` @@ -40,20 +40,19 @@ //! let des: Box = serde_json::from_str(config_file_content).unwrap(); //! let mut model = des.into_model(); //! let (decision, _) = model.next_rwnd().unwrap(); -//! assert_eq!(decision.action, RwndAction::AppRead { bytes: 1024 }); +//! assert_eq!(decision.action, Some(RwndAction::AppRead { bytes: 1024 })); //! let (decision, _) = model.next_rwnd().unwrap(); -//! assert_eq!(decision.action, RwndAction::Remaining { rwnd: 32768 }); +//! assert_eq!(decision.action, Some(RwndAction::Remaining { rwnd: 32768 })); //! let (decision, _) = model.next_rwnd().unwrap(); -//! assert_eq!(decision.action, RwndAction::AppRead { bytes: 1024 }); +//! assert_eq!(decision.action, Some(RwndAction::AppRead { bytes: 1024 })); //! let (decision, _) = model.next_rwnd().unwrap(); -//! assert_eq!(decision.action, RwndAction::Remaining { rwnd: 32768 }); +//! assert_eq!(decision.action, Some(RwndAction::Remaining { rwnd: 32768 })); //! assert_eq!(model.next_rwnd(), None); //! ``` //! -//! Each step must set **exactly one** of `app_read_bytes` or `rwnd_remaining` — -//! never both, never neither. A step that violates this rule fails to deserialize -//! with a clear error message; a programmatically-built config that violates it -//! panics in [`StaticRwndConfig::build`]. +//! At most one of `app_read_bytes` or `rwnd_remaining` may be set per step — +//! never both. A step with neither produces [`RwndDecision::action`] as `None`, +//! which is valid for steps that only reconfigure the receive buffer. use crate::{Duration, RwndAction, RwndDecision, RwndTrace}; use dyn_clone::DynClone; @@ -102,7 +101,7 @@ pub enum RwndActionConfig { /// .build(); /// let (decision, duration) = static_rwnd.next_rwnd().unwrap(); /// assert_eq!(decision.set_rcv_buf, Some(65536)); -/// assert_eq!(decision.action, RwndAction::AppRead { bytes: 1024 }); +/// assert_eq!(decision.action, Some(RwndAction::AppRead { bytes: 1024 })); /// assert_eq!(duration, Duration::from_secs(1)); /// assert_eq!(static_rwnd.next_rwnd(), None); /// ``` @@ -119,14 +118,14 @@ pub struct StaticRwnd { /// `{"duration":"1s","set_rcv_buf":65536,"app_read_bytes":1024}` (or /// `{"duration":"1s","rwnd_remaining":32768}`), never with an `action` wrapper. /// -/// Exactly one of `app_read_bytes` / `rwnd_remaining` must be set; the deserializer -/// rejects both-set and neither-set inputs. +/// At most one of `app_read_bytes` / `rwnd_remaining` may be set; the deserializer +/// rejects inputs where both are present. A step with neither is valid and produces +/// [`RwndDecision::action`] as `None` (useful for steps that only reconfigure the +/// receive buffer). #[derive(Debug, Clone, Default)] pub struct StaticRwndConfig { pub duration: Option, pub set_rcv_buf: Option, - // None only when constructed via `new()`/`Default` and not yet configured; - // `build()` panics on None as a defense for programmatic construction. pub action: Option, } @@ -149,27 +148,23 @@ impl<'de> Deserialize<'de> for StaticRwndConfig { let h = Helper::deserialize(deserializer)?; let action = match (h.app_read_bytes, h.rwnd_remaining) { - (Some(bytes), None) => RwndActionConfig::AppRead { + (Some(bytes), None) => Some(RwndActionConfig::AppRead { app_read_bytes: bytes, - }, - (None, Some(rwnd)) => RwndActionConfig::Remaining { + }), + (None, Some(rwnd)) => Some(RwndActionConfig::Remaining { rwnd_remaining: rwnd, - }, + }), (Some(_), Some(_)) => { return Err(serde::de::Error::custom( "rwnd step cannot set both `app_read_bytes` and `rwnd_remaining`", )); } - (None, None) => { - return Err(serde::de::Error::custom( - "rwnd step must set exactly one of `app_read_bytes` or `rwnd_remaining`", - )); - } + (None, None) => None, }; Ok(Self { duration: h.duration, set_rcv_buf: h.set_rcv_buf, - action: Some(action), + action, }) } } @@ -193,7 +188,11 @@ impl Serialize for StaticRwndConfig { let (app_read_bytes, rwnd_remaining) = match &self.action { Some(RwndActionConfig::AppRead { app_read_bytes }) => (Some(*app_read_bytes), None), Some(RwndActionConfig::Remaining { rwnd_remaining }) => (None, Some(*rwnd_remaining)), - None => (None, None), + None => { + return Err(serde::ser::Error::custom( + "rwnd step must set exactly one of `app_read_bytes` or `rwnd_remaining`", + )); + } }; Out { duration: self.duration, @@ -227,7 +226,7 @@ impl Serialize for StaticRwndConfig { /// let des: Box = serde_json::from_str(config_file_content).unwrap(); /// let mut model = des.into_model(); /// let (decision, _) = model.next_rwnd().unwrap(); -/// assert_eq!(decision.action, RwndAction::AppRead { bytes: 1024 }); +/// assert_eq!(decision.action, Some(RwndAction::AppRead { bytes: 1024 })); /// ``` pub struct RepeatedRwndPattern { pub pattern: Vec>, @@ -322,17 +321,14 @@ impl StaticRwndConfig { } pub fn build(self) -> StaticRwnd { - let action_cfg = self.action.expect( - "StaticRwndConfig::build called without setting one of `app_read_bytes` or `rwnd_remaining`", - ); - let action = match action_cfg { + let action = self.action.map(|cfg| match cfg { RwndActionConfig::AppRead { app_read_bytes } => RwndAction::AppRead { bytes: app_read_bytes, }, RwndActionConfig::Remaining { rwnd_remaining } => RwndAction::Remaining { rwnd: rwnd_remaining, }, - }; + }); StaticRwnd { decision: RwndDecision { set_rcv_buf: self.set_rcv_buf, @@ -401,7 +397,7 @@ mod test { .build(); let (decision, duration) = static_rwnd.next_rwnd().unwrap(); assert_eq!(decision.set_rcv_buf, Some(65536)); - assert_eq!(decision.action, RwndAction::AppRead { bytes: 1024 }); + assert_eq!(decision.action, Some(RwndAction::AppRead { bytes: 1024 })); assert_eq!(duration, Duration::from_secs(1)); assert_eq!(static_rwnd.next_rwnd(), None); } @@ -414,7 +410,7 @@ mod test { .build(); let (decision, duration) = static_rwnd.next_rwnd().unwrap(); assert_eq!(decision.set_rcv_buf, None); - assert_eq!(decision.action, RwndAction::Remaining { rwnd: 32768 }); + assert_eq!(decision.action, Some(RwndAction::Remaining { rwnd: 32768 })); assert_eq!(duration, Duration::from_secs(2)); assert_eq!(static_rwnd.next_rwnd(), None); } @@ -438,14 +434,14 @@ mod test { .count(2) .build(); let next = model.next_rwnd().unwrap(); - assert_eq!(next.0.action, RwndAction::AppRead { bytes: 1024 }); + assert_eq!(next.0.action, Some(RwndAction::AppRead { bytes: 1024 })); assert_eq!(next.1, Duration::from_secs(1)); let next = model.next_rwnd().unwrap(); - assert_eq!(next.0.action, RwndAction::Remaining { rwnd: 32768 }); + assert_eq!(next.0.action, Some(RwndAction::Remaining { rwnd: 32768 })); let next = model.next_rwnd().unwrap(); - assert_eq!(next.0.action, RwndAction::AppRead { bytes: 1024 }); + assert_eq!(next.0.action, Some(RwndAction::AppRead { bytes: 1024 })); let next = model.next_rwnd().unwrap(); - assert_eq!(next.0.action, RwndAction::Remaining { rwnd: 32768 }); + assert_eq!(next.0.action, Some(RwndAction::Remaining { rwnd: 32768 })); assert_eq!(model.next_rwnd(), None); } @@ -469,7 +465,7 @@ mod test { let mut model = des.into_model(); let (decision, duration) = model.next_rwnd().unwrap(); assert_eq!(decision.set_rcv_buf, Some(65536)); - assert_eq!(decision.action, RwndAction::AppRead { bytes: 1024 }); + assert_eq!(decision.action, Some(RwndAction::AppRead { bytes: 1024 })); assert_eq!(duration, Duration::from_secs(1)); } @@ -491,7 +487,7 @@ mod test { let des: Box = serde_json::from_str(&ser_str).unwrap(); let mut model = des.into_model(); let (decision, _) = model.next_rwnd().unwrap(); - assert_eq!(decision.action, RwndAction::Remaining { rwnd: 32768 }); + assert_eq!(decision.action, Some(RwndAction::Remaining { rwnd: 32768 })); } #[test] @@ -512,27 +508,27 @@ mod test { } #[test] - #[cfg(feature = "serde")] - fn test_serde_rejects_neither() { - // Omit duration to avoid the human/non-human format ambiguity; we're testing - // the action constraint, not duration parsing. - let json = "{\"StaticRwndConfig\":{\"set_rcv_buf\":65536}}"; - let result: Result, _> = serde_json::from_str(json); - let err = result - .err() - .expect("deserialization should have failed") - .to_string(); - assert!( - err.contains("exactly one"), - "expected 'exactly one' in error, got: {err}" - ); + fn test_static_rwnd_set_rcv_buf_only() { + let mut model = StaticRwndConfig::new() + .set_rcv_buf(131072) + .duration(Duration::from_secs(1)) + .build(); + let (decision, duration) = model.next_rwnd().unwrap(); + assert_eq!(decision.set_rcv_buf, Some(131072)); + assert_eq!(decision.action, None); + assert_eq!(duration, Duration::from_secs(1)); + assert_eq!(model.next_rwnd(), None); } #[test] - #[should_panic( - expected = "StaticRwndConfig::build called without setting one of `app_read_bytes` or `rwnd_remaining`" - )] - fn test_build_panics_without_action() { - StaticRwndConfig::new().build(); + #[cfg(feature = "serde")] + fn test_serde_action_none_when_neither_set() { + // A step with only set_rcv_buf and no action fields should deserialize to action: None. + let json = "{\"StaticRwndConfig\":{\"set_rcv_buf\":65536}}"; + let des: Box = serde_json::from_str(json).unwrap(); + let mut model = des.into_model(); + let (decision, _) = model.next_rwnd().unwrap(); + assert_eq!(decision.set_rcv_buf, Some(65536)); + assert_eq!(decision.action, None); } } From 785e3aee359f5f0b67a490e1527a0bbda5726667 Mon Sep 17 00:00:00 2001 From: zhc Date: Wed, 15 Jul 2026 09:37:49 +0000 Subject: [PATCH 4/7] refactor(rwnd): use RwndAction directly --- src/model/mod.rs | 2 +- src/model/rwnd.rs | 50 +++++++++-------------------------------------- 2 files changed, 10 insertions(+), 42 deletions(-) diff --git a/src/model/mod.rs b/src/model/mod.rs index b6a8e90..a545f83 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -67,7 +67,7 @@ pub mod rwnd; #[cfg(feature = "rwnd-model")] pub use rwnd::{RepeatedRwndPattern, StaticRwnd}; #[cfg(feature = "rwnd-model")] -pub use rwnd::{RepeatedRwndPatternConfig, RwndActionConfig, RwndTraceConfig, StaticRwndConfig}; +pub use rwnd::{RepeatedRwndPatternConfig, RwndTraceConfig, StaticRwndConfig}; #[cfg(feature = "truncated-normal")] pub mod solve_truncate; diff --git a/src/model/rwnd.rs b/src/model/rwnd.rs index 0c4abe1..08a8357 100644 --- a/src/model/rwnd.rs +++ b/src/model/rwnd.rs @@ -72,21 +72,6 @@ dyn_clone::clone_trait_object!(RwndTraceConfig); #[cfg(feature = "serde")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; -/// The config-layer representation of the action at a rwnd step. -/// -/// This enum is the deserialized form of the mutually-exclusive -/// `app_read_bytes` / `rwnd_remaining` pair. [`StaticRwndConfig`]'s custom -/// serde impls flatten the active variant into the top level of the JSON -/// object, so this enum's own externally-tagged shape is rarely seen by users -/// — but it's serialized/deserialized on its own when used outside the -/// custom container impl. -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq)] -pub enum RwndActionConfig { - AppRead { app_read_bytes: u64 }, - Remaining { rwnd_remaining: u64 }, -} - /// The model of a static rwnd trace: a single decision valid for one duration. /// /// ## Examples @@ -113,8 +98,7 @@ pub struct StaticRwnd { /// The configuration struct for [`StaticRwnd`]. /// -/// The serialized JSON form is **flat** — the active variant of [`RwndActionConfig`] -/// is hoisted to the top level, so a step looks like +/// The serialized JSON form is **flat**: a step looks like /// `{"duration":"1s","set_rcv_buf":65536,"app_read_bytes":1024}` (or /// `{"duration":"1s","rwnd_remaining":32768}`), never with an `action` wrapper. /// @@ -126,7 +110,7 @@ pub struct StaticRwnd { pub struct StaticRwndConfig { pub duration: Option, pub set_rcv_buf: Option, - pub action: Option, + pub action: Option, } #[cfg(feature = "serde")] @@ -148,12 +132,8 @@ impl<'de> Deserialize<'de> for StaticRwndConfig { let h = Helper::deserialize(deserializer)?; let action = match (h.app_read_bytes, h.rwnd_remaining) { - (Some(bytes), None) => Some(RwndActionConfig::AppRead { - app_read_bytes: bytes, - }), - (None, Some(rwnd)) => Some(RwndActionConfig::Remaining { - rwnd_remaining: rwnd, - }), + (Some(bytes), None) => Some(RwndAction::AppRead { bytes }), + (None, Some(rwnd)) => Some(RwndAction::Remaining { rwnd }), (Some(_), Some(_)) => { return Err(serde::de::Error::custom( "rwnd step cannot set both `app_read_bytes` and `rwnd_remaining`", @@ -186,8 +166,8 @@ impl Serialize for StaticRwndConfig { } let (app_read_bytes, rwnd_remaining) = match &self.action { - Some(RwndActionConfig::AppRead { app_read_bytes }) => (Some(*app_read_bytes), None), - Some(RwndActionConfig::Remaining { rwnd_remaining }) => (None, Some(*rwnd_remaining)), + Some(RwndAction::AppRead { bytes }) => (Some(*bytes), None), + Some(RwndAction::Remaining { rwnd }) => (None, Some(*rwnd)), None => { return Err(serde::ser::Error::custom( "rwnd step must set exactly one of `app_read_bytes` or `rwnd_remaining`", @@ -307,32 +287,20 @@ impl StaticRwndConfig { } pub fn app_read(mut self, bytes: u64) -> Self { - self.action = Some(RwndActionConfig::AppRead { - app_read_bytes: bytes, - }); + self.action = Some(RwndAction::AppRead { bytes }); self } pub fn remaining(mut self, rwnd: u64) -> Self { - self.action = Some(RwndActionConfig::Remaining { - rwnd_remaining: rwnd, - }); + self.action = Some(RwndAction::Remaining { rwnd }); self } pub fn build(self) -> StaticRwnd { - let action = self.action.map(|cfg| match cfg { - RwndActionConfig::AppRead { app_read_bytes } => RwndAction::AppRead { - bytes: app_read_bytes, - }, - RwndActionConfig::Remaining { rwnd_remaining } => RwndAction::Remaining { - rwnd: rwnd_remaining, - }, - }); StaticRwnd { decision: RwndDecision { set_rcv_buf: self.set_rcv_buf, - action, + action: self.action, }, duration: Some(self.duration.unwrap_or_else(|| Duration::from_secs(1))), } From 5889a649c37756c93522aa7eebed2c967958b84f Mon Sep 17 00:00:00 2001 From: zhc Date: Wed, 15 Jul 2026 09:43:49 +0000 Subject: [PATCH 5/7] fix(rwnd): replace recursion in repeat pattern with explicit loop --- src/model/rwnd.rs | 47 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/src/model/rwnd.rs b/src/model/rwnd.rs index 08a8357..23c4614 100644 --- a/src/model/rwnd.rs +++ b/src/model/rwnd.rs @@ -242,25 +242,36 @@ impl RwndTrace for StaticRwnd { impl RwndTrace for RepeatedRwndPattern { fn next_rwnd(&mut self) -> Option<(RwndDecision, Duration)> { - if self.pattern.is_empty() || (self.count != 0 && self.current_cycle >= self.count) { - None - } else { + let pattern_len = self.pattern.len(); + // Allow at most pattern_len + 1 consecutive inner-None results before + // giving up. The +1 covers a possibly-exhausted current_model at entry; + // after that, each remaining slot is a fresh clone whose behaviour is + // deterministic. If all pattern_len fresh clones return None, the + // pattern will never produce a value regardless of count. + let mut budget = pattern_len + 1; + loop { + if pattern_len == 0 || (self.count != 0 && self.current_cycle >= self.count) { + return None; + } + if budget == 0 { + return None; + } if self.current_model.is_none() { self.current_model = Some(self.pattern[self.current_pattern].clone().into_model()); } match self.current_model.as_mut().unwrap().next_rwnd() { - Some(rwnd) => Some(rwnd), + Some(item) => return Some(item), None => { self.current_model = None; + budget -= 1; self.current_pattern += 1; - if self.current_pattern >= self.pattern.len() { + if self.current_pattern >= pattern_len { self.current_pattern = 0; self.current_cycle += 1; if self.count != 0 && self.current_cycle >= self.count { return None; } } - self.next_rwnd() } } } @@ -499,4 +510,28 @@ mod test { assert_eq!(decision.set_rcv_buf, Some(65536)); assert_eq!(decision.action, None); } + + #[test] + fn test_repeated_rwnd_pattern_all_zero_duration_terminates() { + // All inner models have duration == 0 and return None immediately. + // With count == 0 (infinite repeat) the old recursive implementation + // would spin forever; the loop-based one must return None promptly. + let pat = vec![ + Box::new( + StaticRwndConfig::new() + .app_read(1024) + .duration(Duration::ZERO), + ) as Box, + Box::new( + StaticRwndConfig::new() + .remaining(32768) + .duration(Duration::ZERO), + ) as Box, + ]; + let mut model = RepeatedRwndPatternConfig::new() + .pattern(pat) + .count(0) // infinite + .build(); + assert_eq!(model.next_rwnd(), None); + } } From cce9c56e40fe8c3c182e28d3735697d9f5aedd87 Mon Sep 17 00:00:00 2001 From: zhc Date: Wed, 15 Jul 2026 09:55:51 +0000 Subject: [PATCH 6/7] =?UTF-8?q?fix(rwnd):=20allow=20serializing=20StaticRw?= =?UTF-8?q?ndConfig=20with=20action=EF=BC=9A=20None?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/model/rwnd.rs | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/model/rwnd.rs b/src/model/rwnd.rs index 23c4614..62506db 100644 --- a/src/model/rwnd.rs +++ b/src/model/rwnd.rs @@ -168,11 +168,7 @@ impl Serialize for StaticRwndConfig { let (app_read_bytes, rwnd_remaining) = match &self.action { Some(RwndAction::AppRead { bytes }) => (Some(*bytes), None), Some(RwndAction::Remaining { rwnd }) => (None, Some(*rwnd)), - None => { - return Err(serde::ser::Error::custom( - "rwnd step must set exactly one of `app_read_bytes` or `rwnd_remaining`", - )); - } + None => (None, None), }; Out { duration: self.duration, @@ -499,6 +495,31 @@ mod test { assert_eq!(model.next_rwnd(), None); } + #[test] + #[cfg(feature = "serde")] + fn test_serde_roundtrip_set_rcv_buf_only() { + let cfg = Box::new( + StaticRwndConfig::new() + .set_rcv_buf(131072) + .duration(Duration::from_secs(1)), + ) as Box; + let ser_str = serde_json::to_string(&cfg).unwrap(); + #[cfg(feature = "human")] + let expected = "{\"StaticRwndConfig\":{\"duration\":\"1s\",\"set_rcv_buf\":131072}}"; + #[cfg(not(feature = "human"))] + let expected = + "{\"StaticRwndConfig\":{\"duration\":{\"secs\":1,\"nanos\":0},\"set_rcv_buf\":131072}}"; + assert_eq!(ser_str, expected); + + let des: Box = serde_json::from_str(&ser_str).unwrap(); + let mut model = des.into_model(); + let (decision, duration) = model.next_rwnd().unwrap(); + assert_eq!(decision.set_rcv_buf, Some(131072)); + assert_eq!(decision.action, None); + assert_eq!(duration, Duration::from_secs(1)); + assert_eq!(model.next_rwnd(), None); + } + #[test] #[cfg(feature = "serde")] fn test_serde_action_none_when_neither_set() { From 54e708c73e7d5b0a153816cfc3917716fb35a8c4 Mon Sep 17 00:00:00 2001 From: zhc Date: Wed, 15 Jul 2026 10:15:22 +0000 Subject: [PATCH 7/7] build(deps): bump typetag minimum version to 0.2.22 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index bdd7957..1057c64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ rand_distr = { version = "0.5.1", optional = true } serde = { version = "1.0", features = ["derive"], optional = true } serde_json = { version = "1.0", optional = true } statrs = { version = "0.18.0", optional = true } -typetag = { version = "0.2.5", optional = true } +typetag = { version = "0.2.22", optional = true } [dev-dependencies] figment = { version = "0.10.19", features = ["json"] }