Skip to content

feat(model): add a rwnd trace trait and models#26

Merged
BobAnkh merged 7 commits into
mainfrom
rwnd_trace
Jul 15, 2026
Merged

feat(model): add a rwnd trace trait and models#26
BobAnkh merged 7 commits into
mainfrom
rwnd_trace

Conversation

@zhang-hc22

@zhang-hc22 zhang-hc22 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

This PR solves issue #25

Description

This PR adds receive window (rwnd) trace support to netem-trace, enabling simulation and replay of TCP receiver-side behaviour over time.

Each step in a rwnd trace is described by four fields:

  • duration — how long this configuration lasts before the next step applies
  • set_rcv_buf (optional) — resize the socket's receive buffer to this value at this step
  • app_read_bytes (optional) — simulate the application reading this many bytes from the receive buffer (drives the receiver model)
  • rwnd_remaining (optional) — directly assert the observed remaining receive window after consumption (useful for replaying captured traces)

At most one of app_read_bytes / rwnd_remaining may be set per step — never both. A step may also set neither, e.g. a step that only reconfigures the receive buffer; this is a valid, first-class case (not a workaround), represented as action: None. This invariant is enforced at the type/serde level: deserialization rejects only the both-set case with a clear error message.

New public surface in src/lib.rs:

  • RwndAction — enum encoding the action per step (AppRead { bytes: u64 } / Remaining { rwnd: u64 })
  • RwndDecision — a single step's payload: set_rcv_buf: Option<u64> + action: Option<RwndAction>, both independently optional
  • RwndTrace — trait mirroring BwTrace/DelayTrace/LossTrace, returning Option<(RwndDecision, Duration)>

(No separate Rwnd type alias — byte-count fields use u64 directly, since set_rcv_buf/app_read_bytes/rwnd_remaining describe unrelated quantities and a shared alias was misleading.)

New model module src/model/rwnd.rs (enabled via rwnd-model / model feature):

  • StaticRwnd / StaticRwndConfig — single-step model with builder API (set_rcv_buf(), app_read(), remaining(), duration())
  • RepeatedRwndPattern / RepeatedRwndPatternConfig — repeating pattern over a sequence of steps, with infinite-loop (count = 0) support. Iteration uses an explicit bounded loop rather than recursion, so a pattern made entirely of immediately-exhausted steps (e.g. duration == 0) terminates instead of overflowing the stack, regardless of count.
  • RwndTraceConfig — config-to-model conversion trait, typetag-registered for serde polymorphism
  • Custom Serialize/Deserialize for StaticRwndConfig that flattens app_read_bytes/rwnd_remaining to the top level of the JSON object (consistent with the flat style of other models), and round-trips correctly whether or not an action is set

The rwnd-model feature is added to the model umbrella feature and follows the same dependency pattern as loss-model and duplicate-model (dep:dyn-clone).

Dependency bump: typetag minimum version raised to 0.2.22 (from 0.2.5). Older typetag-impl releases generate impl blocks that trigger the non_local_definitions lint on every #[cfg_attr(feature = "serde", typetag::serde)]-annotated trait in this crate; 0.2.22 suppresses this internally.

How Has This Been Tested

Unit tests (src/model/rwnd.rs #[cfg(test)] block):

  • test_static_rwnd_model_app_read / test_static_rwnd_model_remaining — verifies each action variant's fields and single-step exhaustion
  • test_static_rwnd_set_rcv_buf_only — verifies a step with only set_rcv_buf set produces action: None
  • test_repeated_rwnd_pattern — verifies a 2-step pattern repeated twice, then None
  • test_repeated_rwnd_pattern_all_zero_duration_terminates — regression test: a pattern of all-duration == 0 steps with count = 0 (infinite) must return None promptly instead of recursing forever
  • test_serde_roundtrip_app_read / test_serde_roundtrip_remaining / test_serde_roundtrip_set_rcv_buf_only — serialize → deserialize → assert field equality, including the action: None case (#[cfg(feature = "serde")])
  • test_serde_rejects_both — confirms deserialization error when both app_read_bytes and rwnd_remaining are present
  • test_serde_action_none_when_neither_set — confirms a step with neither field deserializes to action: None rather than erroring

Doc tests on all public types and the module-level examples.

Tests run against --features "model,serde" (without human) and --features "model,serde,human" to confirm both duration-format paths work correctly. (Serde tests that check error messages omit duration to avoid the human/non-human format ambiguity, since that's not what those tests are checking.)

cargo test --features "model,serde" # 24 unit + 47 doc — all pass
cargo test --features "model,serde,human" # 25 unit + 47 doc — all pass

Types of changes

  • New feature (non-breaking change which adds functionality)

Checklist

  • Code follows the code style of this project.
  • Changes follow the CONTRIBUTING guidelines.
  • Update necessary documentation accordingly.
  • Lint and tests pass locally with the changes.
  • Check issues and pull requests first. You don't want to duplicate effort.

Other information

The RwndAction / RwndDecision types are placed in lib.rs alongside BwTrace, LossTrace, etc., matching the existing convention. The rwnd-model feature follows the same opt-in pattern as other model features and is included in the model umbrella and full feature set.

This design went through a review pass that removed two premature abstractions (a Rwnd type alias and a RwndActionConfig config-layer enum duplicating RwndAction) and closed three correctness gaps: RwndDecision.action is now genuinely optional instead of requiring a dummy AppRead { bytes: 0 } for buffer-only steps, RepeatedRwndPattern no longer risks stack overflow on all-empty patterns, and StaticRwndConfig serialization no longer errors on the now-valid action: None case.

@zhang-hc22
zhang-hc22 requested a review from Centaurus99 July 1, 2026 03:07
@zhang-hc22 zhang-hc22 linked an issue Jul 1, 2026 that may be closed by this pull request
@zhang-hc22 zhang-hc22 self-assigned this Jul 1, 2026
@zhang-hc22

Copy link
Copy Markdown
Contributor Author

@Centaurus99 Please have a look of this PR.

Comment thread src/lib.rs Outdated
Comment thread src/lib.rs Outdated
Comment thread src/model/rwnd.rs Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds receive-window (rwnd) trace support to netem-trace by introducing new public rwnd trace types/traits, a new rwnd model module with static + repeated-pattern implementations, and wiring the new model behind a rwnd-model feature included in the model umbrella.

Changes:

  • Introduces public rwnd trace surface (Rwnd, RwndAction, RwndDecision, RwndTrace) in src/lib.rs.
  • Adds src/model/rwnd.rs implementing StaticRwnd and RepeatedRwndPattern plus serde support and unit tests.
  • Wires rwnd-model into feature flags and the model module exports.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
src/model/rwnd.rs New rwnd trace model implementations, serde (de)serialization, and unit tests
src/model/mod.rs Exposes the new rwnd model module/types behind rwnd-model
src/lib.rs Adds public rwnd trace types/traits to the crate API
Cargo.toml Adds rwnd-model feature and includes it in the model feature umbrella

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/lib.rs
Comment thread src/lib.rs Outdated
Comment thread src/model/rwnd.rs
Comment thread src/model/rwnd.rs

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Comment thread src/model/rwnd.rs
Comment thread src/lib.rs

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

Comment thread src/model/rwnd.rs
Comment on lines +485 to +496
#[test]
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);
}
Comment thread src/model/rwnd.rs
Comment on lines +500 to +505
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<dyn RwndTraceConfig>;
Comment thread src/model/rwnd.rs
Comment on lines +523 to +533
#[test]
#[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<dyn RwndTraceConfig> = 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);
}
Comment thread src/lib.rs
Comment on lines +241 to +259
/// The action a rwnd trace instructs the receiver to take at a single step.
///
/// 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
/// 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 },
}
Comment thread src/lib.rs
Comment on lines +269 to +272
/// If `Some`, reconfigure the socket's receive buffer to this size at this step.
pub set_rcv_buf: Option<u64>,
/// If `Some`, the app-read or observed-remaining action for this step.
pub action: Option<RwndAction>,
Comment thread src/model/rwnd.rs
"rwnd step cannot set both `app_read_bytes` and `rwnd_remaining`",
));
}
(None, None) => None,
Comment thread src/model/rwnd.rs
Comment on lines +308 to +311
decision: RwndDecision {
set_rcv_buf: self.set_rcv_buf,
action: self.action,
},
@BobAnkh BobAnkh changed the title feat(rwnd): add rwnd trace and its unit test feat(model): add a rwnd trace trait and models Jul 15, 2026
@BobAnkh
BobAnkh merged commit 746da0e into main Jul 15, 2026
9 checks passed
@BobAnkh
BobAnkh deleted the rwnd_trace branch July 15, 2026 10:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[enhancement] Support Rwnd trace

4 participants