Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion noq-proto/src/connection/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ pub use streams::{
ShouldTransmit, StreamEvent, Streams, WriteError,
};

mod timer;
pub(crate) mod timer;
use timer::{Timer, TimerTable};

mod transmit_buf;
Expand Down Expand Up @@ -455,6 +455,15 @@ impl Connection {
self.timers.peek()
}

/// Returns the instant at which `timer` is armed to fire, or `None` if it is not.
///
/// `None` covers the timer never having been armed, having fired, and having been
/// cancelled.
#[cfg(test)]
pub(crate) fn timer_pending(&self, timer: Timer) -> Option<Instant> {
self.timers.get(timer)
}

/// Returns application-facing events
///
/// Connections should be polled for events after:
Expand Down
8 changes: 7 additions & 1 deletion noq-proto/src/tests/multipath.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use assert_matches::assert_matches;
use testresult::TestResult;
use tracing::info;

use crate::connection::timer::{PathTimer, Timer};
use crate::{
ClientConfig, ConnectionId, ConnectionIdGenerator, Endpoint, EndpointConfig, FourTuple,
LOCAL_CID_COUNT, NetworkChangeHint, PathId, PathStatus, RandomConnectionIdGenerator,
Expand Down Expand Up @@ -451,10 +452,15 @@ fn open_path_validation_fails_server_side() -> TestResult {

info!("manual keep-alive of PathId::ZERO");
pair.ping_path(Client, PathId::ZERO)?;
// Sent here, before the clock moves: `drive_until_timer` advances time before it
// drives, so a queued keep-alive would otherwise go out at the deadline itself.
pair.drive();

info!("advancing time to past client path {path_id} idle");
pair.advance_time();
pair.drive_until_timer(Client, Timer::PerPath(path_id, PathTimer::PathIdle));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't understand why there are now two drives right after each other?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

They aren't the same work — measured rather than guessed:

The PathIdle timer fires while the loop is driving the client, and the abandonment still needs steps to reach the server: drive_until_timer returns with 3 steps of traffic still queued (while pair.step() {} immediately after it, on several seeds). So the trailing drive() is what puts the abandonment in front of the server. Without it poll(Server) is None because the server never heard about the abandonment, not because it stayed quiet — the assertion would pass vacuously.

The drive() above drive_until_timer is load-bearing in the other direction. Each step advances the clock before driving, so without it the keep-alive ping goes out at the deadline instead of before it. Seed 0, path 0 frame_tx.ping: with the drive the loop takes one step, to 8006ms, ping already sent; without it the first step lands at 8005ms with the ping still unsent, and it goes out on the next step at 8006ms.

Both variants pass 4000 seeds, which is why I left a comment at each site instead of deleting one. Comments are in 6c0d7ea.

// Deliver the abandonment to the server: the timer fires while driving the client, and
// the loop drives the server before that packet arrives. Without this step the
// `poll(Server)` check below would pass simply because the server never heard about it.
pair.drive();

// The client gave up first and timed out.
Expand Down
35 changes: 35 additions & 0 deletions noq-proto/src/tests/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use rustls::{
use tracing::{debug, info, info_span, trace};

use crate::crypto::rustls::{QuicClientConfig, QuicServerConfig, configured_provider};
use crate::connection::timer::Timer;
use crate::{
ClientConfig, ClosePathError, ClosedPath, Connection, ConnectionError, ConnectionEvent,
ConnectionHandle, ConnectionStats, DatagramEvent, Datagrams, Dir, Duration, EcnCodepoint,
Expand Down Expand Up @@ -766,6 +767,40 @@ impl ConnPair {
self.conn_mut(side).poll_timeout()
}

/// Advances virtual time and drives both endpoints until `timer` fires on `side`.
///
/// [`Self::drive`] refuses to step past a point where the only timers still pending
/// are idle timers (see `Connection::is_idle`), so it cannot be used to reach an idle
/// timeout: the timeout is exactly what `is_idle` discounts. This loop keeps stepping
/// to each next wakeup until `timer` is no longer armed.
///
/// Note that timers fire in `drive_client`/`drive_server`, not in `advance_time`, so
/// every step has to drive both endpoints. A timer that is *cancelled* rather than
/// fired also ends the loop; callers should still assert on the event they expect, so
/// that a cancellation fails loudly instead of passing silently.
///
/// Each step advances the clock *before* driving, so packets that must be sent at the
/// current time have to be driven out by a preceding [`Self::drive`]; otherwise they
/// are queued until after the first step.
///
/// Panics if the timer is still armed after 1024 steps.
#[track_caller]
pub(super) fn drive_until_timer(&mut self, side: Side, timer: Timer) {
let mut steps = 0;
while self.conn(side).timer_pending(timer).is_some() {
// If `timer` is armed, this endpoint has a wakeup scheduled, so the advance
// cannot run out of timers.
assert!(
self.advance_time(),
"{timer:?} is armed but no endpoint has a wakeup scheduled"
);
self.drive_client();
self.drive_server();
steps += 1;
assert!(steps < 1024, "{timer:?} still armed after {steps} steps");
}
}

pub(super) fn poll(&mut self, side: Side) -> Option<Event> {
self.conn_mut(side).poll()
}
Expand Down
Loading