refactor(irtt-client): move cadence into managed runner
This commit is contained in:
31 files changed
+710
-4100
No files matched your search
@@ -207,7 +207,7 @@ enum OutputRow {
|
|||||||
client_send_wall: SystemTime,
|
client_send_wall: SystemTime,
|
||||||
bytes: usize,
|
bytes: usize,
|
||||||
send_call: Duration,
|
send_call: Duration,
|
||||||
timer_error: Duration,
|
timer_error: Option<Duration>,
|
||||||
},
|
},
|
||||||
EchoReply(ReplyRow),
|
EchoReply(ReplyRow),
|
||||||
Loss {
|
Loss {
|
||||||
@@ -950,7 +950,7 @@ fn cell_for(row: &OutputRow, column: Column, context: RenderContext<'_>) -> Opti
|
|||||||
},
|
},
|
||||||
Column::TimerErrorUs => match row {
|
Column::TimerErrorUs => match row {
|
||||||
OutputRow::EchoSent { timer_error, .. } => {
|
OutputRow::EchoSent { timer_error, .. } => {
|
||||||
Some(CellValue::Unsigned(duration_us(*timer_error)))
|
timer_error.map(|error| CellValue::Unsigned(duration_us(error)))
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -170,11 +170,11 @@ mod tests {
|
|||||||
collector.process(&ClientEvent::EchoSent {
|
collector.process(&ClientEvent::EchoSent {
|
||||||
seq: 1,
|
seq: 1,
|
||||||
remote: test_remote(),
|
remote: test_remote(),
|
||||||
scheduled_at: sent_at.mono,
|
scheduled_at: Some(sent_at.mono),
|
||||||
sent_at,
|
sent_at,
|
||||||
bytes: 64,
|
bytes: 64,
|
||||||
send_call: Duration::from_micros(10),
|
send_call: Duration::from_micros(10),
|
||||||
timer_error: Duration::from_micros(2),
|
timer_error: Some(Duration::from_micros(2)),
|
||||||
});
|
});
|
||||||
collector.process(&ClientEvent::EchoReply {
|
collector.process(&ClientEvent::EchoReply {
|
||||||
seq: 1,
|
seq: 1,
|
||||||
@@ -251,11 +251,11 @@ mod tests {
|
|||||||
collector.process(&ClientEvent::EchoSent {
|
collector.process(&ClientEvent::EchoSent {
|
||||||
seq,
|
seq,
|
||||||
remote: test_remote(),
|
remote: test_remote(),
|
||||||
scheduled_at: sent_at.mono,
|
scheduled_at: Some(sent_at.mono),
|
||||||
sent_at,
|
sent_at,
|
||||||
bytes: 64,
|
bytes: 64,
|
||||||
send_call: Duration::from_micros(send_call_us),
|
send_call: Duration::from_micros(send_call_us),
|
||||||
timer_error: Duration::from_micros(timer_error_us),
|
timer_error: Some(Duration::from_micros(timer_error_us)),
|
||||||
});
|
});
|
||||||
collector.process(&ClientEvent::EchoReply {
|
collector.process(&ClientEvent::EchoReply {
|
||||||
seq,
|
seq,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use irtt_client::{Client, ClientConfig, ClientEvent};
|
use irtt_client::{Client, ClientConfig, ClientEvent, OpenOutcome};
|
||||||
|
|
||||||
const RUN_DURATION: Duration = Duration::from_secs(2);
|
const RUN_DURATION: Duration = Duration::from_secs(2);
|
||||||
|
|
||||||
@@ -45,32 +45,42 @@ fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match client.open() {
|
let negotiated = match client.open() {
|
||||||
Ok(outcome) => println!("session opened: {outcome:?}"),
|
Ok(OpenOutcome::Started {
|
||||||
|
event, negotiated, ..
|
||||||
|
}) => {
|
||||||
|
println!("session opened: {event:?}");
|
||||||
|
negotiated
|
||||||
|
}
|
||||||
|
Ok(OpenOutcome::NoTestCompleted { event, .. }) => {
|
||||||
|
println!("{event:?}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("open failed (is a server running at 127.0.0.1:2112?): {err}");
|
eprintln!("open failed (is a server running at 127.0.0.1:2112?): {err}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// The run itself lasts `duration`, but a lost probe isn't classified as
|
// This loop owns cadence and duration. The transport sends whenever called.
|
||||||
// `EchoLoss` until `probe_timeout()` after it was sent; wait at least
|
let interval = Duration::from_nanos(negotiated.params.interval_ns as u64);
|
||||||
// that much past the run's own end so a loss near the end can still be
|
let start = Instant::now();
|
||||||
// observed and printed before this example gives up and closes.
|
let end = start + Duration::from_nanos(negotiated.params.duration_ns as u64);
|
||||||
let deadline = Instant::now() + RUN_DURATION + client.probe_timeout();
|
let deadline = end + client.probe_timeout();
|
||||||
while Instant::now() < deadline && !client.is_run_complete() {
|
let mut next_send = start;
|
||||||
// send_probe is caller-paced: only call it once its own schedule
|
while Instant::now() < deadline && !client.is_peer_closed() {
|
||||||
// says a probe is due, rather than as fast as the loop spins.
|
let now = Instant::now();
|
||||||
if client
|
if now >= end && !client.has_pending_probes() {
|
||||||
.next_send_deadline()
|
break;
|
||||||
.is_some_and(|send_at| Instant::now() >= send_at)
|
}
|
||||||
{
|
if now < end && now >= next_send {
|
||||||
// A failed send retains its prepared probe without advancing the
|
|
||||||
// schedule, so a persistent error (e.g. the interface going
|
|
||||||
// down) would otherwise make this loop retry silently until the
|
|
||||||
// safety deadline instead of reporting it.
|
|
||||||
match client.send_probe() {
|
match client.send_probe() {
|
||||||
Ok(events) => events.iter().for_each(print_event),
|
Ok(events) => {
|
||||||
|
events.iter().for_each(print_event);
|
||||||
|
while next_send <= Instant::now() {
|
||||||
|
next_send += interval;
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("send failed: {err}");
|
eprintln!("send failed: {err}");
|
||||||
break;
|
break;
|
||||||
@@ -93,6 +103,9 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if client.is_peer_closed() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
match client.close() {
|
match client.close() {
|
||||||
Ok(events) => events.iter().for_each(print_event),
|
Ok(events) => events.iter().for_each(print_event),
|
||||||
Err(err) => eprintln!("close failed: {err}"),
|
Err(err) => eprintln!("close failed: {err}"),
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use irtt_client::{AsyncClient, ClientConfig, ClientEvent};
|
use irtt_client::{AsyncClient, ClientConfig, ClientEvent, OpenOutcome};
|
||||||
|
|
||||||
/// Upper bound on how long the receive wait can go without also calling
|
/// Upper bound on how long the receive wait can go without also calling
|
||||||
/// `poll_timeouts()`. `AsyncClient` does not expose the earliest pending
|
/// `poll_timeouts()`. `AsyncClient` does not expose the earliest pending
|
||||||
@@ -58,24 +58,43 @@ async fn run() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match client.open().await {
|
let negotiated = match client.open().await {
|
||||||
Ok(outcome) => println!("session opened: {outcome:?}"),
|
Ok(OpenOutcome::Started {
|
||||||
|
event, negotiated, ..
|
||||||
|
}) => {
|
||||||
|
println!("session opened: {event:?}");
|
||||||
|
negotiated
|
||||||
|
}
|
||||||
|
Ok(OpenOutcome::NoTestCompleted { event, .. }) => {
|
||||||
|
println!("{event:?}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("open failed (is a server running at 127.0.0.1:2112?): {err}");
|
eprintln!("open failed (is a server running at 127.0.0.1:2112?): {err}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
while !client.is_run_complete() {
|
// This loop owns cadence and duration. The transport sends whenever called.
|
||||||
|
let interval = Duration::from_nanos(negotiated.params.interval_ns as u64);
|
||||||
|
let start = Instant::now();
|
||||||
|
let end = start + Duration::from_nanos(negotiated.params.duration_ns as u64);
|
||||||
|
let deadline = end + client.probe_timeout();
|
||||||
|
let mut next_send = start;
|
||||||
|
while Instant::now() < deadline && !client.is_peer_closed() {
|
||||||
|
if Instant::now() >= end && !client.has_pending_probes() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
// Bound the receive wait by the next send/timeout deadline, rather
|
// Bound the receive wait by the next send/timeout deadline, rather
|
||||||
// than awaiting recv() unconditionally: a lost or rate-limited reply
|
// than awaiting recv() unconditionally: a lost or rate-limited reply
|
||||||
// must not stall pacing or timeout classification indefinitely.
|
// must not stall pacing or timeout classification indefinitely.
|
||||||
let wake_at = client
|
let wake_at = (next_send < end)
|
||||||
.next_send_deadline()
|
.then_some(next_send)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
.chain(client.next_probe_timeout_deadline())
|
||||||
.chain(std::iter::once(Instant::now() + TIMEOUT_POLL_INTERVAL))
|
.chain(std::iter::once(Instant::now() + TIMEOUT_POLL_INTERVAL))
|
||||||
.min()
|
.min()
|
||||||
.expect("the fixed poll-interval deadline is always present");
|
.unwrap();
|
||||||
|
|
||||||
let recv_result = tokio::select! {
|
let recv_result = tokio::select! {
|
||||||
events = client.recv() => Some(events),
|
events = client.recv() => Some(events),
|
||||||
@@ -91,16 +110,14 @@ async fn run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if client
|
if Instant::now() < end && Instant::now() >= next_send {
|
||||||
.next_send_deadline()
|
|
||||||
.is_some_and(|send_at| Instant::now() >= send_at)
|
|
||||||
{
|
|
||||||
// A failed send retains its prepared probe without advancing the
|
|
||||||
// schedule, so a persistent error (e.g. the interface going
|
|
||||||
// down) would otherwise make this loop retry forever instead of
|
|
||||||
// reporting it.
|
|
||||||
match client.send_probe().await {
|
match client.send_probe().await {
|
||||||
Ok(events) => events.iter().for_each(print_event),
|
Ok(events) => {
|
||||||
|
events.iter().for_each(print_event);
|
||||||
|
while next_send <= Instant::now() {
|
||||||
|
next_send += interval;
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("send failed: {err}");
|
eprintln!("send failed: {err}");
|
||||||
break;
|
break;
|
||||||
@@ -116,6 +133,9 @@ async fn run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if client.is_peer_closed() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
match client.close().await {
|
match client.close().await {
|
||||||
Ok(events) => events.iter().for_each(print_event),
|
Ok(events) => events.iter().for_each(print_event),
|
||||||
Err(err) => eprintln!("close failed: {err}"),
|
Err(err) => eprintln!("close failed: {err}"),
|
||||||
|
|||||||
@@ -7,20 +7,10 @@ use std::{
|
|||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
use std::{
|
|
||||||
cell::{Cell, RefCell},
|
|
||||||
collections::VecDeque,
|
|
||||||
};
|
|
||||||
|
|
||||||
use tokio::time;
|
use tokio::time;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
client::{
|
client::{echo_sent_event, validate_datagram_length},
|
||||||
echo_sent_event,
|
|
||||||
schedule::{instant_abs_diff, ProbeSchedule},
|
|
||||||
validate_datagram_length,
|
|
||||||
},
|
|
||||||
config::ClientConfig,
|
config::ClientConfig,
|
||||||
error::ClientError,
|
error::ClientError,
|
||||||
event::{ClientEvent, OpenOutcome},
|
event::{ClientEvent, OpenOutcome},
|
||||||
@@ -54,13 +44,9 @@ const OPEN_POLL_WORK_BUDGET: usize = 64;
|
|||||||
/// this crate already bounds open-attempt work keeps that impossible.
|
/// this crate already bounds open-attempt work keeps that impossible.
|
||||||
const RECV_POLL_WORK_BUDGET: usize = 64;
|
const RECV_POLL_WORK_BUDGET: usize = 64;
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
use crate::client::ProbeSendTimestamps;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct PreparedAsyncOpen {
|
struct PreparedAsyncOpen {
|
||||||
machine: PreparedOpenAcceptance,
|
machine: PreparedOpenAcceptance,
|
||||||
schedule: Option<ProbeSchedule>,
|
|
||||||
recv_buffer_len: Option<usize>,
|
recv_buffer_len: Option<usize>,
|
||||||
negotiated_traffic_class: Option<u8>,
|
negotiated_traffic_class: Option<u8>,
|
||||||
}
|
}
|
||||||
@@ -156,11 +142,6 @@ impl AsyncOpenState {
|
|||||||
self.stopped_after_current_attempt
|
self.stopped_after_current_attempt
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn has_retained_cleanup(&self) -> bool {
|
|
||||||
self.cleanup.is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deadline(&self) -> Instant {
|
fn deadline(&self) -> Instant {
|
||||||
self.deadline
|
self.deadline
|
||||||
.expect("an open attempt has an active deadline")
|
.expect("an open attempt has an active deadline")
|
||||||
@@ -173,70 +154,6 @@ impl AsyncOpenState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[derive(Debug)]
|
|
||||||
enum InjectedSend {
|
|
||||||
WouldBlock,
|
|
||||||
Error,
|
|
||||||
ReportedLength(usize),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
struct AsyncClientTestHooks {
|
|
||||||
sends: RefCell<VecDeque<InjectedSend>>,
|
|
||||||
send_attempts: Cell<usize>,
|
|
||||||
receive_would_block: Cell<usize>,
|
|
||||||
/// Number of subsequent receive attempts (`poll_open` or `poll_recv`)
|
|
||||||
/// that should report `io::ErrorKind::Interrupted` instead of performing
|
|
||||||
/// a real nonblocking receive, deterministically exercising interrupted-
|
|
||||||
/// syscall retry behavior.
|
|
||||||
inject_recv_interrupted: Cell<usize>,
|
|
||||||
fail_open_dscp: Cell<bool>,
|
|
||||||
fail_cleanup_send: Cell<bool>,
|
|
||||||
fail_peer_close_dscp: Cell<bool>,
|
|
||||||
fail_dscp_restore: Cell<bool>,
|
|
||||||
pause_open_cleanup_before_send: Cell<bool>,
|
|
||||||
pause_open_before_writable: Cell<bool>,
|
|
||||||
pause_open_before_readable: Cell<bool>,
|
|
||||||
pause_probe_before_writable: Cell<bool>,
|
|
||||||
pause_close_before_writable: Cell<bool>,
|
|
||||||
pause_close_after_would_block: Cell<bool>,
|
|
||||||
probe_timestamps: RefCell<VecDeque<ProbeSendTimestamps>>,
|
|
||||||
close_sent_at: Cell<Option<ClientTimestamp>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
impl AsyncClientTestHooks {
|
|
||||||
fn try_send(&self, socket: &tokio::net::UdpSocket, packet: &[u8]) -> io::Result<usize> {
|
|
||||||
self.send_attempts.set(self.send_attempts.get() + 1);
|
|
||||||
if let Some(injected) = self.sends.borrow_mut().pop_front() {
|
|
||||||
return match injected {
|
|
||||||
InjectedSend::WouldBlock => Err(io::Error::from(io::ErrorKind::WouldBlock)),
|
|
||||||
InjectedSend::Error => Err(io::Error::other("injected send failure")),
|
|
||||||
InjectedSend::ReportedLength(bytes) => Ok(bytes),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
socket.try_send(packet)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn take_probe_timestamps(&self) -> Option<ProbeSendTimestamps> {
|
|
||||||
self.probe_timestamps.borrow_mut().pop_front()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Consumes one pending injected interruption, if any, reporting whether
|
|
||||||
/// the caller should report `io::ErrorKind::Interrupted` instead of
|
|
||||||
/// attempting a real receive.
|
|
||||||
fn take_injected_interrupted(&self) -> bool {
|
|
||||||
let remaining = self.inject_recv_interrupted.get();
|
|
||||||
if remaining == 0 {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
self.inject_recv_interrupted.set(remaining - 1);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Low-level Tokio IRTT client for one connected UDP target.
|
/// Low-level Tokio IRTT client for one connected UDP target.
|
||||||
///
|
///
|
||||||
/// `AsyncClient` does not construct, own, or store a Tokio runtime. Its async
|
/// `AsyncClient` does not construct, own, or store a Tokio runtime. Its async
|
||||||
@@ -271,7 +188,6 @@ impl AsyncClientTestHooks {
|
|||||||
pub struct AsyncClient {
|
pub struct AsyncClient {
|
||||||
socket: tokio::net::UdpSocket,
|
socket: tokio::net::UdpSocket,
|
||||||
machine: SessionMachine,
|
machine: SessionMachine,
|
||||||
schedule: Option<ProbeSchedule>,
|
|
||||||
remote: SocketAddr,
|
remote: SocketAddr,
|
||||||
recv_buffer: Vec<u8>,
|
recv_buffer: Vec<u8>,
|
||||||
applied_traffic_class: Option<u8>,
|
applied_traffic_class: Option<u8>,
|
||||||
@@ -280,8 +196,6 @@ pub struct AsyncClient {
|
|||||||
/// See `Client::tx_timestamping_enabled` in `client.rs` for the
|
/// See `Client::tx_timestamping_enabled` in `client.rs` for the
|
||||||
/// invariant this tracks; it is the same adapter-owned capability flag.
|
/// invariant this tracks; it is the same adapter-owned capability flag.
|
||||||
tx_timestamping_enabled: bool,
|
tx_timestamping_enabled: bool,
|
||||||
#[cfg(test)]
|
|
||||||
test_hooks: AsyncClientTestHooks,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsyncClient {
|
impl AsyncClient {
|
||||||
@@ -302,15 +216,12 @@ impl AsyncClient {
|
|||||||
Ok(Self {
|
Ok(Self {
|
||||||
socket,
|
socket,
|
||||||
machine,
|
machine,
|
||||||
schedule: None,
|
|
||||||
remote,
|
remote,
|
||||||
recv_buffer: vec![0_u8; recv_buffer_size(false, None)?],
|
recv_buffer: vec![0_u8; recv_buffer_size(false, None)?],
|
||||||
applied_traffic_class: None,
|
applied_traffic_class: None,
|
||||||
prepared_open: Some(prepared_open),
|
prepared_open: Some(prepared_open),
|
||||||
prepared_probe: None,
|
prepared_probe: None,
|
||||||
tx_timestamping_enabled: false,
|
tx_timestamping_enabled: false,
|
||||||
#[cfg(test)]
|
|
||||||
test_hooks: AsyncClientTestHooks::default(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,16 +233,16 @@ impl AsyncClient {
|
|||||||
/// ignored without resending the request.
|
/// ignored without resending the request.
|
||||||
///
|
///
|
||||||
/// Dropping this future before trusted acceptance leaves the local machine
|
/// Dropping this future before trusted acceptance leaves the local machine
|
||||||
/// connected with no committed schedule or negotiated adapter state. If it
|
/// connected with no negotiated adapter state. If it is dropped during
|
||||||
/// is dropped during best-effort post-token cleanup, cleanup may remain
|
/// best-effort post-token cleanup, cleanup may remain incomplete because
|
||||||
/// incomplete because this low-level client never detaches cleanup work.
|
/// this low-level client never detaches cleanup work.
|
||||||
pub async fn open(&mut self) -> Result<OpenOutcome, ClientError> {
|
pub async fn open(&mut self) -> Result<OpenOutcome, ClientError> {
|
||||||
let mut state = AsyncOpenState::new();
|
let mut state = AsyncOpenState::new();
|
||||||
poll_fn(|cx| self.poll_open(&mut state, cx)).await
|
poll_fn(|cx| self.poll_open(&mut state, cx)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send one caller-paced probe with transactional protocol and schedule
|
/// Send one echo probe now, committing protocol state only after acceptance.
|
||||||
/// commits.
|
/// The caller owns pacing and run duration.
|
||||||
///
|
///
|
||||||
/// A prepared packet is retained across readiness false positives,
|
/// A prepared packet is retained across readiness false positives,
|
||||||
/// cancellation, and socket errors so a later call retries the same logical
|
/// cancellation, and socket errors so a later call retries the same logical
|
||||||
@@ -412,10 +323,6 @@ impl AsyncClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !state.request_submitted {
|
if !state.request_submitted {
|
||||||
#[cfg(test)]
|
|
||||||
if self.test_hooks.pause_open_before_writable.replace(false) {
|
|
||||||
return Poll::Pending;
|
|
||||||
}
|
|
||||||
match poll_writable_until(&self.socket, state.deadline_timer(), cx) {
|
match poll_writable_until(&self.socket, state.deadline_timer(), cx) {
|
||||||
Poll::Pending => return Poll::Pending,
|
Poll::Pending => return Poll::Pending,
|
||||||
Poll::Ready(Err(error)) => return Poll::Ready(Err(error)),
|
Poll::Ready(Err(error)) => return Poll::Ready(Err(error)),
|
||||||
@@ -430,9 +337,6 @@ impl AsyncClient {
|
|||||||
.prepared_open
|
.prepared_open
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("connected clients retain their prepared open request");
|
.expect("connected clients retain their prepared open request");
|
||||||
#[cfg(test)]
|
|
||||||
let send_result = self.test_hooks.try_send(&self.socket, &request.bytes);
|
|
||||||
#[cfg(not(test))]
|
|
||||||
let send_result = self.socket.try_send(&request.bytes);
|
let send_result = self.socket.try_send(&request.bytes);
|
||||||
match send_result {
|
match send_result {
|
||||||
Ok(bytes) => {
|
Ok(bytes) => {
|
||||||
@@ -446,10 +350,6 @@ impl AsyncClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
if self.test_hooks.pause_open_before_readable.replace(false) {
|
|
||||||
return Poll::Pending;
|
|
||||||
}
|
|
||||||
match poll_readable_until(&self.socket, state.deadline_timer(), cx) {
|
match poll_readable_until(&self.socket, state.deadline_timer(), cx) {
|
||||||
Poll::Pending => return Poll::Pending,
|
Poll::Pending => return Poll::Pending,
|
||||||
Poll::Ready(Err(error)) => return Poll::Ready(Err(error)),
|
Poll::Ready(Err(error)) => return Poll::Ready(Err(error)),
|
||||||
@@ -463,10 +363,6 @@ impl AsyncClient {
|
|||||||
Poll::Ready(Ok(true)) => {}
|
Poll::Ready(Ok(true)) => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
if self.test_hooks.take_injected_interrupted() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let datagram = match try_recv_tokio_datagram(&self.socket, &mut state.buffer) {
|
let datagram = match try_recv_tokio_datagram(&self.socket, &mut state.buffer) {
|
||||||
Ok(datagram) => datagram,
|
Ok(datagram) => datagram,
|
||||||
// WouldBlock is a readiness false positive; Interrupted
|
// WouldBlock is a readiness false positive; Interrupted
|
||||||
@@ -514,7 +410,7 @@ impl AsyncClient {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let prepared = match self.prepare_async_open(machine, datagram.received_at) {
|
let prepared = match self.prepare_async_open(machine) {
|
||||||
Ok(prepared) => prepared,
|
Ok(prepared) => prepared,
|
||||||
Err(failure) => {
|
Err(failure) => {
|
||||||
state.cleanup = Some(AsyncOpenCleanup::Adapter(failure));
|
state.cleanup = Some(AsyncOpenCleanup::Adapter(failure));
|
||||||
@@ -539,19 +435,11 @@ impl AsyncClient {
|
|||||||
}
|
}
|
||||||
if self.prepared_probe.is_none() {
|
if self.prepared_probe.is_none() {
|
||||||
match self.machine.prepare_probe() {
|
match self.machine.prepare_probe() {
|
||||||
Ok(prepared) => self.prepared_probe = prepared,
|
Ok(prepared) => self.prepared_probe = Some(prepared),
|
||||||
Err(error) => return Poll::Ready(Err(error)),
|
Err(error) => return Poll::Ready(Err(error)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if self.prepared_probe.is_none() {
|
|
||||||
return Poll::Ready(Ok(Vec::new()));
|
|
||||||
}
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
#[cfg(test)]
|
|
||||||
if self.test_hooks.pause_probe_before_writable.replace(false) {
|
|
||||||
return Poll::Pending;
|
|
||||||
}
|
|
||||||
match self.socket.poll_send_ready(cx) {
|
match self.socket.poll_send_ready(cx) {
|
||||||
Poll::Pending => return Poll::Pending,
|
Poll::Pending => return Poll::Pending,
|
||||||
Poll::Ready(Ok(())) => {}
|
Poll::Ready(Ok(())) => {}
|
||||||
@@ -560,23 +448,6 @@ impl AsyncClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
let test_timestamps = self.test_hooks.take_probe_timestamps();
|
|
||||||
#[cfg(not(test))]
|
|
||||||
let permission_at = Instant::now();
|
|
||||||
#[cfg(test)]
|
|
||||||
let permission_at = test_timestamps
|
|
||||||
.map(|timestamps| timestamps.permission_at)
|
|
||||||
.unwrap_or_else(Instant::now);
|
|
||||||
|
|
||||||
let schedule = self
|
|
||||||
.schedule
|
|
||||||
.as_mut()
|
|
||||||
.expect("open sessions always have a probe schedule");
|
|
||||||
if !schedule.permit_probe_at(permission_at) {
|
|
||||||
self.prepared_probe = None;
|
|
||||||
return Poll::Ready(Ok(Vec::new()));
|
|
||||||
}
|
|
||||||
let prepared = self
|
let prepared = self
|
||||||
.prepared_probe
|
.prepared_probe
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -585,11 +456,6 @@ impl AsyncClient {
|
|||||||
Ok(preflight) => preflight,
|
Ok(preflight) => preflight,
|
||||||
Err(error) => return Poll::Ready(Err(error)),
|
Err(error) => return Poll::Ready(Err(error)),
|
||||||
};
|
};
|
||||||
let schedule_commit =
|
|
||||||
match schedule.preflight_caller_commit(machine_preflight.next_packets_sent) {
|
|
||||||
Ok(commit) => commit,
|
|
||||||
Err(error) => return Poll::Ready(Err(error)),
|
|
||||||
};
|
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
if let Err(source) = events.try_reserve(1) {
|
if let Err(source) = events.try_reserve(1) {
|
||||||
return Poll::Ready(Err(ClientError::AllocationFailed {
|
return Poll::Ready(Err(ClientError::AllocationFailed {
|
||||||
@@ -597,20 +463,14 @@ impl AsyncClient {
|
|||||||
source,
|
source,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
let expected_bytes = prepared.bytes.len();
|
let expected_bytes = prepared.bytes.len();
|
||||||
let scheduled_at = schedule_commit.scheduled_at;
|
|
||||||
// Private pre-send anchor: finalizes all fallible commit work
|
// Private pre-send anchor: finalizes all fallible commit work
|
||||||
// (timeout deadline arithmetic, kernel TX lower bound) before
|
// (timeout deadline arithmetic, kernel TX lower bound) before
|
||||||
// the send attempt. A WouldBlock retry samples a fresh one on
|
// the send attempt. A WouldBlock retry samples a fresh one on
|
||||||
// the next loop iteration; only the successful attempt's anchor
|
// the next loop iteration; only the successful attempt's anchor
|
||||||
// is retained (via `machine_commit`/`commit_probe_sent` below).
|
// is retained (via `machine_commit`/`commit_probe_sent` below).
|
||||||
#[cfg(not(test))]
|
|
||||||
let send_anchor = ClientTimestamp::now();
|
let send_anchor = ClientTimestamp::now();
|
||||||
#[cfg(test)]
|
|
||||||
let send_anchor = test_timestamps
|
|
||||||
.map(|timestamps| timestamps.send_anchor)
|
|
||||||
.unwrap_or_else(ClientTimestamp::now);
|
|
||||||
let machine_commit = match self
|
let machine_commit = match self
|
||||||
.machine
|
.machine
|
||||||
.finalize_probe_commit(machine_preflight, send_anchor)
|
.finalize_probe_commit(machine_preflight, send_anchor)
|
||||||
@@ -618,20 +478,11 @@ impl AsyncClient {
|
|||||||
Ok(commit) => commit,
|
Ok(commit) => commit,
|
||||||
Err(error) => return Poll::Ready(Err(error)),
|
Err(error) => return Poll::Ready(Err(error)),
|
||||||
};
|
};
|
||||||
#[cfg(not(test))]
|
|
||||||
let send_call_start = Instant::now();
|
let send_call_start = Instant::now();
|
||||||
#[cfg(test)]
|
|
||||||
let send_call_start = test_timestamps
|
|
||||||
.map(|timestamps| timestamps.send_call_start)
|
|
||||||
.unwrap_or_else(Instant::now);
|
|
||||||
#[cfg(test)]
|
|
||||||
let send_result = self.test_hooks.try_send(&self.socket, &prepared.bytes);
|
|
||||||
#[cfg(not(test))]
|
|
||||||
let send_result = self.socket.try_send(&prepared.bytes);
|
let send_result = self.socket.try_send(&prepared.bytes);
|
||||||
let bytes = match send_result {
|
let bytes = match send_result {
|
||||||
Ok(bytes) => bytes,
|
Ok(bytes) => bytes,
|
||||||
// No sent_at measurement, no machine commit, no schedule
|
// No measurement or machine commit: retry the transaction
|
||||||
// commit: the transaction is fully discarded and retried
|
|
||||||
// with a fresh send_anchor on the next loop iteration.
|
// with a fresh send_anchor on the next loop iteration.
|
||||||
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
|
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
|
||||||
self.machine.invalidate_kernel_tx_correlation();
|
self.machine.invalidate_kernel_tx_correlation();
|
||||||
@@ -642,43 +493,25 @@ impl AsyncClient {
|
|||||||
return Poll::Ready(Err(ClientError::Socket(error)));
|
return Poll::Ready(Err(ClientError::Socket(error)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(not(test))]
|
|
||||||
let send_finished_at = Instant::now();
|
let send_finished_at = Instant::now();
|
||||||
#[cfg(test)]
|
|
||||||
let send_finished_at = test_timestamps
|
|
||||||
.map(|timestamps| timestamps.send_finished_at)
|
|
||||||
.unwrap_or_else(Instant::now);
|
|
||||||
// Public post-send measurement, captured only after the
|
// Public post-send measurement, captured only after the
|
||||||
// successful send above. The subsequent machine/schedule commit
|
// successful send above. The subsequent machine commit
|
||||||
// is infallible.
|
// is infallible.
|
||||||
#[cfg(not(test))]
|
|
||||||
let sent_at = ClientTimestamp::now();
|
let sent_at = ClientTimestamp::now();
|
||||||
#[cfg(test)]
|
|
||||||
let sent_at = test_timestamps
|
|
||||||
.map(|timestamps| timestamps.sent_at)
|
|
||||||
.unwrap_or_else(ClientTimestamp::now);
|
|
||||||
let sent = self
|
let sent = self
|
||||||
.machine
|
.machine
|
||||||
.commit_probe_sent(machine_commit, sent_at, bytes);
|
.commit_probe_sent(machine_commit, sent_at, bytes);
|
||||||
schedule.commit(schedule_commit);
|
|
||||||
self.prepared_probe = None;
|
self.prepared_probe = None;
|
||||||
|
|
||||||
let send_call = send_finished_at.saturating_duration_since(send_call_start);
|
let send_call = send_finished_at.saturating_duration_since(send_call_start);
|
||||||
let timer_error = instant_abs_diff(sent_at.mono, scheduled_at);
|
|
||||||
if let Err(error) = validate_datagram_length(expected_bytes, bytes) {
|
if let Err(error) = validate_datagram_length(expected_bytes, bytes) {
|
||||||
return Poll::Ready(Err(error));
|
return Poll::Ready(Err(error));
|
||||||
}
|
}
|
||||||
if let Err(error) = self.drain_tx_timestamps() {
|
if let Err(error) = self.drain_tx_timestamps() {
|
||||||
return Poll::Ready(Err(error));
|
return Poll::Ready(Err(error));
|
||||||
}
|
}
|
||||||
events.push(echo_sent_event(
|
events.push(echo_sent_event(self.remote, sent, send_call));
|
||||||
self.remote,
|
|
||||||
sent,
|
|
||||||
send_call,
|
|
||||||
scheduled_at,
|
|
||||||
timer_error,
|
|
||||||
));
|
|
||||||
return Poll::Ready(Ok(events));
|
return Poll::Ready(Ok(events));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -699,17 +532,7 @@ impl AsyncClient {
|
|||||||
return Poll::Ready(Err(ClientError::Socket(error)));
|
return Poll::Ready(Err(ClientError::Socket(error)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[cfg(test)]
|
|
||||||
if self.test_hooks.receive_would_block.get() > 0 {
|
|
||||||
self.test_hooks
|
|
||||||
.receive_would_block
|
|
||||||
.set(self.test_hooks.receive_would_block.get() - 1);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
#[cfg(test)]
|
|
||||||
if self.test_hooks.take_injected_interrupted() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let datagram = match try_recv_tokio_datagram(&self.socket, &mut self.recv_buffer) {
|
let datagram = match try_recv_tokio_datagram(&self.socket, &mut self.recv_buffer) {
|
||||||
Ok(datagram) => datagram,
|
Ok(datagram) => datagram,
|
||||||
// See `RECV_POLL_WORK_BUDGET`: both a readiness false
|
// See `RECV_POLL_WORK_BUDGET`: both a readiness false
|
||||||
@@ -743,7 +566,6 @@ impl AsyncClient {
|
|||||||
Err(error) => return Poll::Ready(Err(error)),
|
Err(error) => return Poll::Ready(Err(error)),
|
||||||
};
|
};
|
||||||
if self.machine.is_peer_closed() {
|
if self.machine.is_peer_closed() {
|
||||||
self.schedule = None;
|
|
||||||
self.prepared_probe = None;
|
self.prepared_probe = None;
|
||||||
if self.clear_peer_close_dscp().is_ok() {
|
if self.clear_peer_close_dscp().is_ok() {
|
||||||
self.applied_traffic_class = None;
|
self.applied_traffic_class = None;
|
||||||
@@ -778,10 +600,6 @@ impl AsyncClient {
|
|||||||
let previous_traffic_class = self.applied_traffic_class;
|
let previous_traffic_class = self.applied_traffic_class;
|
||||||
let expected_bytes = prepared.bytes.len();
|
let expected_bytes = prepared.bytes.len();
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
if self.test_hooks.pause_close_before_writable.replace(false) {
|
|
||||||
return Poll::Pending;
|
|
||||||
}
|
|
||||||
match self.socket.poll_send_ready(cx) {
|
match self.socket.poll_send_ready(cx) {
|
||||||
Poll::Pending => return Poll::Pending,
|
Poll::Pending => return Poll::Pending,
|
||||||
Poll::Ready(Ok(())) => {}
|
Poll::Ready(Ok(())) => {}
|
||||||
@@ -792,16 +610,8 @@ impl AsyncClient {
|
|||||||
if let Err(error) = self.clear_close_dscp() {
|
if let Err(error) = self.clear_close_dscp() {
|
||||||
return Poll::Ready(Err(error));
|
return Poll::Ready(Err(error));
|
||||||
}
|
}
|
||||||
let mut rollback = DscpRollback::armed(
|
let mut rollback =
|
||||||
&self.socket,
|
DscpRollback::armed(&self.socket, self.remote, previous_traffic_class);
|
||||||
self.remote,
|
|
||||||
previous_traffic_class,
|
|
||||||
#[cfg(test)]
|
|
||||||
&self.test_hooks.fail_dscp_restore,
|
|
||||||
);
|
|
||||||
#[cfg(test)]
|
|
||||||
let send_result = self.test_hooks.try_send(&self.socket, prepared.bytes);
|
|
||||||
#[cfg(not(test))]
|
|
||||||
let send_result = self.socket.try_send(prepared.bytes);
|
let send_result = self.socket.try_send(prepared.bytes);
|
||||||
let bytes = match send_result {
|
let bytes = match send_result {
|
||||||
Ok(bytes) => bytes,
|
Ok(bytes) => bytes,
|
||||||
@@ -809,28 +619,18 @@ impl AsyncClient {
|
|||||||
if let Err(error) = rollback.restore() {
|
if let Err(error) = rollback.restore() {
|
||||||
return Poll::Ready(Err(error));
|
return Poll::Ready(Err(error));
|
||||||
}
|
}
|
||||||
#[cfg(test)]
|
|
||||||
if self.test_hooks.pause_close_after_would_block.replace(false) {
|
|
||||||
return Poll::Pending;
|
|
||||||
}
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Err(error) => return Poll::Ready(Err(ClientError::Socket(error))),
|
Err(error) => return Poll::Ready(Err(ClientError::Socket(error))),
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(not(test))]
|
|
||||||
let close_sent_at = ClientTimestamp::now();
|
let close_sent_at = ClientTimestamp::now();
|
||||||
#[cfg(test)]
|
|
||||||
let close_sent_at = self
|
|
||||||
.test_hooks
|
|
||||||
.close_sent_at
|
|
||||||
.take()
|
|
||||||
.unwrap_or_else(ClientTimestamp::now);
|
|
||||||
let event = self
|
let event = self
|
||||||
.machine
|
.machine
|
||||||
.commit_local_close(prepared.commit, close_sent_at);
|
.commit_local_close(prepared.commit, close_sent_at);
|
||||||
rollback.disarm();
|
rollback.disarm();
|
||||||
self.schedule = None;
|
|
||||||
self.prepared_probe = None;
|
self.prepared_probe = None;
|
||||||
self.applied_traffic_class = None;
|
self.applied_traffic_class = None;
|
||||||
|
|
||||||
@@ -842,33 +642,19 @@ impl AsyncClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the next scheduled probe deadline.
|
|
||||||
pub fn next_send_deadline(&self) -> Option<Instant> {
|
|
||||||
if !self.machine.is_open() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
self.schedule.as_ref()?.next_send_deadline()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn probe_interval(&self) -> Option<Duration> {
|
|
||||||
if !self.machine.is_open() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
self.schedule.as_ref().map(ProbeSchedule::interval)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the configured local probe timeout.
|
/// Return the configured local probe timeout.
|
||||||
pub fn probe_timeout(&self) -> Duration {
|
pub fn probe_timeout(&self) -> Duration {
|
||||||
self.machine.probe_timeout()
|
self.machine.probe_timeout()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return whether the current run has completed.
|
/// Negotiated parameters of the currently open session.
|
||||||
pub fn is_run_complete(&self) -> bool {
|
pub fn negotiated_params(&self) -> Option<&crate::NegotiatedParams> {
|
||||||
self.machine.is_terminal()
|
self.machine.negotiated_params()
|
||||||
|| self
|
}
|
||||||
.schedule
|
|
||||||
.as_ref()
|
/// Whether any sent probes still await a reply or timeout.
|
||||||
.is_some_and(|schedule| schedule.is_finished() && self.machine.pending_is_empty())
|
pub fn has_pending_probes(&self) -> bool {
|
||||||
|
!self.machine.pending_is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return whether an authenticated peer close ended the session.
|
/// Return whether an authenticated peer close ended the session.
|
||||||
@@ -884,7 +670,8 @@ impl AsyncClient {
|
|||||||
self.machine.packets_sent()
|
self.machine.packets_sent()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn next_probe_timeout_deadline(&self) -> Option<Instant> {
|
/// Earliest timeout deadline among probes still awaiting a reply.
|
||||||
|
pub fn next_probe_timeout_deadline(&self) -> Option<Instant> {
|
||||||
self.machine.next_probe_timeout_deadline()
|
self.machine.next_probe_timeout_deadline()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -896,64 +683,17 @@ impl AsyncClient {
|
|||||||
self.prepared_probe = None;
|
self.prepared_probe = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn retain_open_cleanup_once(&self) {
|
|
||||||
self.test_hooks.fail_open_dscp.set(true);
|
|
||||||
self.test_hooks.pause_open_cleanup_before_send.set(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn poll_recv_ready_for_test(&self, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
|
||||||
self.socket.poll_recv_ready(cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn remove_pending_for_test(
|
|
||||||
&mut self,
|
|
||||||
wire_seq: u32,
|
|
||||||
) -> Option<crate::probe::PendingProbe> {
|
|
||||||
self.machine.remove_pending_for_test(wire_seq)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn replace_pending_for_test(&mut self, probe: crate::probe::PendingProbe) {
|
|
||||||
self.machine.replace_pending_for_test(probe);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn skip_missed_probe_slots_at(&mut self, now: Instant) -> Result<(), ClientError> {
|
|
||||||
let Some(schedule) = self.schedule.as_mut() else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let Some(deadline) = schedule.next_send_deadline() else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
if deadline > now {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let commit = schedule.preflight_managed_commit(deadline, now)?;
|
|
||||||
schedule.commit(commit);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_async_open(
|
fn prepare_async_open(
|
||||||
&mut self,
|
&mut self,
|
||||||
machine: PreparedOpenAcceptance,
|
machine: PreparedOpenAcceptance,
|
||||||
opened_at: ClientTimestamp,
|
|
||||||
) -> Result<PreparedAsyncOpen, Box<PreparedAsyncOpenFailure>> {
|
) -> Result<PreparedAsyncOpen, Box<PreparedAsyncOpenFailure>> {
|
||||||
let Some(negotiated) = machine.normal_negotiated() else {
|
let Some(negotiated) = machine.normal_negotiated() else {
|
||||||
return Ok(PreparedAsyncOpen {
|
return Ok(PreparedAsyncOpen {
|
||||||
machine,
|
machine,
|
||||||
schedule: None,
|
|
||||||
recv_buffer_len: None,
|
recv_buffer_len: None,
|
||||||
negotiated_traffic_class: None,
|
negotiated_traffic_class: None,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
let schedule = match ProbeSchedule::new(opened_at.mono, negotiated) {
|
|
||||||
Ok(schedule) => schedule,
|
|
||||||
Err(primary) => {
|
|
||||||
return Err(Box::new(PreparedAsyncOpenFailure { primary, machine }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let recv_buffer_len = match recv_buffer_size(self.machine.has_hmac(), Some(negotiated)) {
|
let recv_buffer_len = match recv_buffer_size(self.machine.has_hmac(), Some(negotiated)) {
|
||||||
Ok(size) => size,
|
Ok(size) => size,
|
||||||
Err(primary) => {
|
Err(primary) => {
|
||||||
@@ -981,17 +721,7 @@ impl AsyncClient {
|
|||||||
machine,
|
machine,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
#[cfg(test)]
|
|
||||||
let dscp_result = if self.test_hooks.fail_open_dscp.replace(false) {
|
|
||||||
Err(ClientError::SocketOption {
|
|
||||||
operation: "set negotiated DSCP",
|
|
||||||
remote: self.remote,
|
|
||||||
source: io::Error::other("injected negotiated DSCP failure"),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
apply_traffic_class_to_tokio_socket(&self.socket, self.remote, negotiated_traffic_class)
|
|
||||||
};
|
|
||||||
#[cfg(not(test))]
|
|
||||||
let dscp_result = apply_traffic_class_to_tokio_socket(
|
let dscp_result = apply_traffic_class_to_tokio_socket(
|
||||||
&self.socket,
|
&self.socket,
|
||||||
self.remote,
|
self.remote,
|
||||||
@@ -1004,7 +734,6 @@ impl AsyncClient {
|
|||||||
|
|
||||||
Ok(PreparedAsyncOpen {
|
Ok(PreparedAsyncOpen {
|
||||||
machine,
|
machine,
|
||||||
schedule: Some(schedule),
|
|
||||||
recv_buffer_len: Some(recv_buffer_len),
|
recv_buffer_len: Some(recv_buffer_len),
|
||||||
negotiated_traffic_class: Some(negotiated_traffic_class),
|
negotiated_traffic_class: Some(negotiated_traffic_class),
|
||||||
})
|
})
|
||||||
@@ -1015,7 +744,7 @@ impl AsyncClient {
|
|||||||
self.recv_buffer.resize(recv_buffer_len, 0);
|
self.recv_buffer.resize(recv_buffer_len, 0);
|
||||||
}
|
}
|
||||||
let outcome = self.machine.commit_open(prepared.machine);
|
let outcome = self.machine.commit_open(prepared.machine);
|
||||||
self.schedule = prepared.schedule;
|
|
||||||
self.applied_traffic_class = prepared.negotiated_traffic_class;
|
self.applied_traffic_class = prepared.negotiated_traffic_class;
|
||||||
self.prepared_open = None;
|
self.prepared_open = None;
|
||||||
self.tx_timestamping_enabled = try_enable_tx_timestamping(&self.socket);
|
self.tx_timestamping_enabled = try_enable_tx_timestamping(&self.socket);
|
||||||
@@ -1048,23 +777,6 @@ impl AsyncClient {
|
|||||||
else {
|
else {
|
||||||
return Poll::Ready(Err(state.cleanup.take().unwrap().into_primary()));
|
return Poll::Ready(Err(state.cleanup.take().unwrap().into_primary()));
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
if self
|
|
||||||
.test_hooks
|
|
||||||
.pause_open_cleanup_before_send
|
|
||||||
.replace(false)
|
|
||||||
{
|
|
||||||
return Poll::Pending;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
if self.test_hooks.fail_cleanup_send.replace(false) {
|
|
||||||
return Poll::Ready(Err(state.cleanup.take().unwrap().into_primary()));
|
|
||||||
}
|
|
||||||
#[cfg(test)]
|
|
||||||
let send_result = self.test_hooks.try_send(&self.socket, packet);
|
|
||||||
#[cfg(not(test))]
|
|
||||||
let send_result = self.socket.try_send(packet);
|
let send_result = self.socket.try_send(packet);
|
||||||
match send_result {
|
match send_result {
|
||||||
Ok(_) => return Poll::Ready(Err(state.cleanup.take().unwrap().into_primary())),
|
Ok(_) => return Poll::Ready(Err(state.cleanup.take().unwrap().into_primary())),
|
||||||
@@ -1087,14 +799,6 @@ impl AsyncClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn clear_peer_close_dscp(&self) -> Result<(), ClientError> {
|
fn clear_peer_close_dscp(&self) -> Result<(), ClientError> {
|
||||||
#[cfg(test)]
|
|
||||||
if self.test_hooks.fail_peer_close_dscp.replace(false) {
|
|
||||||
return Err(ClientError::SocketOption {
|
|
||||||
operation: "clear DSCP before close",
|
|
||||||
remote: self.remote,
|
|
||||||
source: io::Error::other("injected peer-close DSCP cleanup failure"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
clear_dscp_on_tokio_socket(&self.socket, self.remote)
|
clear_dscp_on_tokio_socket(&self.socket, self.remote)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1117,8 +821,6 @@ struct DscpRollback<'a> {
|
|||||||
remote: SocketAddr,
|
remote: SocketAddr,
|
||||||
previous_traffic_class: Option<u8>,
|
previous_traffic_class: Option<u8>,
|
||||||
armed: bool,
|
armed: bool,
|
||||||
#[cfg(test)]
|
|
||||||
fail_restore: &'a Cell<bool>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> DscpRollback<'a> {
|
impl<'a> DscpRollback<'a> {
|
||||||
@@ -1126,15 +828,12 @@ impl<'a> DscpRollback<'a> {
|
|||||||
socket: &'a tokio::net::UdpSocket,
|
socket: &'a tokio::net::UdpSocket,
|
||||||
remote: SocketAddr,
|
remote: SocketAddr,
|
||||||
previous_traffic_class: Option<u8>,
|
previous_traffic_class: Option<u8>,
|
||||||
#[cfg(test)] fail_restore: &'a Cell<bool>,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
socket,
|
socket,
|
||||||
remote,
|
remote,
|
||||||
previous_traffic_class,
|
previous_traffic_class,
|
||||||
armed: true,
|
armed: true,
|
||||||
#[cfg(test)]
|
|
||||||
fail_restore,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1142,14 +841,7 @@ impl<'a> DscpRollback<'a> {
|
|||||||
if !self.armed {
|
if !self.armed {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
#[cfg(test)]
|
|
||||||
if self.fail_restore.replace(false) {
|
|
||||||
return Err(ClientError::SocketOption {
|
|
||||||
operation: "restore negotiated DSCP",
|
|
||||||
remote: self.remote,
|
|
||||||
source: io::Error::other("injected DSCP restore failure"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
match self.previous_traffic_class {
|
match self.previous_traffic_class {
|
||||||
Some(traffic_class) => {
|
Some(traffic_class) => {
|
||||||
apply_traffic_class_to_tokio_socket(self.socket, self.remote, traffic_class)?
|
apply_traffic_class_to_tokio_socket(self.socket, self.remote, traffic_class)?
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ use std::{
|
|||||||
sync::mpsc,
|
sync::mpsc,
|
||||||
task::{Context, Poll, Waker},
|
task::{Context, Poll, Waker},
|
||||||
thread::{self, JoinHandle},
|
thread::{self, JoinHandle},
|
||||||
time::SystemTime,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use irtt_proto::{
|
use irtt_proto::{
|
||||||
@@ -18,10 +17,7 @@ use tokio::runtime::{Builder, Runtime};
|
|||||||
mod in_tree_server;
|
mod in_tree_server;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{socket_options::tokio_socket_traffic_class, Client, RunMode, SocketConfig};
|
||||||
probe::PendingProbe, socket_options::tokio_socket_traffic_class, Client, ClientTimestamp,
|
|
||||||
RunMode, SocketConfig,
|
|
||||||
};
|
|
||||||
use in_tree_server::InTreeServer;
|
use in_tree_server::InTreeServer;
|
||||||
use irtt_server::ServerConfig;
|
use irtt_server::ServerConfig;
|
||||||
|
|
||||||
@@ -72,10 +68,6 @@ async fn opened_client(addr: SocketAddr, dscp: u8) -> AsyncClient {
|
|||||||
client
|
client
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inject_send(client: &AsyncClient, result: InjectedSend) {
|
|
||||||
client.test_hooks.sends.borrow_mut().push_back(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn socket_dscp(client: &AsyncClient) -> u32 {
|
fn socket_dscp(client: &AsyncClient) -> u32 {
|
||||||
tokio_socket_traffic_class(&client.socket, client.remote).unwrap() >> 2
|
tokio_socket_traffic_class(&client.socket, client.remote).unwrap() >> 2
|
||||||
}
|
}
|
||||||
@@ -208,15 +200,6 @@ fn echo_reply(
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_open_server(key: Option<Vec<u8>>, packets_after_open: usize) -> TestServer {
|
|
||||||
start_server(move |socket, tx| {
|
|
||||||
open_session(&socket, &tx, key.as_deref());
|
|
||||||
for _ in 0..packets_after_open {
|
|
||||||
let _ = recv_packet(&socket, &tx);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start_no_test_server() -> TestServer {
|
fn start_no_test_server() -> TestServer {
|
||||||
start_server(move |socket, tx| {
|
start_server(move |socket, tx| {
|
||||||
let (open_packet, peer) = recv_packet(&socket, &tx);
|
let (open_packet, peer) = recv_packet(&socket, &tx);
|
||||||
@@ -285,124 +268,6 @@ fn recv_before_open_fails_on_first_poll_without_socket_readiness() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn open_ignores_malformed_and_bad_hmac_without_resending() {
|
|
||||||
let key = b"open-filter-key".to_vec();
|
|
||||||
let server_key = key.clone();
|
|
||||||
let server = start_server(move |socket, tx| {
|
|
||||||
let (open_packet, peer) = recv_packet(&socket, &tx);
|
|
||||||
let (params, _) = open_request(&open_packet, Some(&server_key));
|
|
||||||
socket.send_to(&[0_u8], peer).unwrap();
|
|
||||||
let mut bad_hmac = encode_open_reply(
|
|
||||||
&OpenReply {
|
|
||||||
flags: flags::FLAG_OPEN | flags::FLAG_REPLY,
|
|
||||||
token: TOKEN,
|
|
||||||
params: params.clone(),
|
|
||||||
},
|
|
||||||
Some(&server_key),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
bad_hmac[4] ^= 0xff;
|
|
||||||
socket.send_to(&bad_hmac, peer).unwrap();
|
|
||||||
send_open_reply(
|
|
||||||
&socket,
|
|
||||||
peer,
|
|
||||||
params,
|
|
||||||
Some(&server_key),
|
|
||||||
flags::FLAG_OPEN | flags::FLAG_REPLY,
|
|
||||||
TOKEN,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
runtime().block_on(async {
|
|
||||||
let mut client = AsyncClient::connect(config(server.addr, Some(key), 0))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
inject_send(&client, InjectedSend::WouldBlock);
|
|
||||||
let attempts = client.test_hooks.send_attempts.get();
|
|
||||||
assert!(matches!(
|
|
||||||
client.open().await.unwrap(),
|
|
||||||
OpenOutcome::Started { .. }
|
|
||||||
));
|
|
||||||
assert_eq!(client.test_hooks.send_attempts.get(), attempts + 2);
|
|
||||||
assert!(client.prepared_open.is_none());
|
|
||||||
assert!(client.schedule.is_some());
|
|
||||||
});
|
|
||||||
assert_eq!(server.finish().len(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn ignored_open_traffic_uses_one_request_and_deadline_per_attempt() {
|
|
||||||
let server = start_server(move |socket, tx| {
|
|
||||||
for _ in 0..2 {
|
|
||||||
let (_, peer) = recv_packet(&socket, &tx);
|
|
||||||
for _ in 0..3 {
|
|
||||||
socket.send_to(&[0_u8], peer).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let mut client_config = config(server.addr, None, 0);
|
|
||||||
client_config.open_timeouts = vec![Duration::from_millis(200), Duration::from_millis(200)];
|
|
||||||
|
|
||||||
runtime().block_on(async {
|
|
||||||
let mut client = AsyncClient::connect(client_config).await.unwrap();
|
|
||||||
assert!(matches!(client.open().await, Err(ClientError::OpenTimeout)));
|
|
||||||
assert!(client.machine.prepare_open_request().is_ok());
|
|
||||||
assert!(client.schedule.is_none());
|
|
||||||
});
|
|
||||||
assert_eq!(server.finish().len(), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn opening_poll_budget_yields_then_public_open_completes() {
|
|
||||||
let (queued, queue_ready) = mpsc::channel();
|
|
||||||
let server = start_server(move |socket, tx| {
|
|
||||||
let (open_packet, peer) = recv_packet(&socket, &tx);
|
|
||||||
let (params, _) = open_request(&open_packet, None);
|
|
||||||
for _ in 0..=OPEN_POLL_WORK_BUDGET {
|
|
||||||
socket.send_to(&[0_u8], peer).unwrap();
|
|
||||||
}
|
|
||||||
send_open_reply(
|
|
||||||
&socket,
|
|
||||||
peer,
|
|
||||||
params,
|
|
||||||
None,
|
|
||||||
flags::FLAG_OPEN | flags::FLAG_REPLY,
|
|
||||||
TOKEN,
|
|
||||||
);
|
|
||||||
queued.send(()).unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
runtime().block_on(async {
|
|
||||||
let mut client = AsyncClient::connect(config(server.addr, None, 0))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
client.socket.writable().await.unwrap();
|
|
||||||
client.test_hooks.pause_open_before_readable.set(true);
|
|
||||||
let mut opening = Box::pin(client.open());
|
|
||||||
|
|
||||||
// Submit the one opening request, then let the server queue all noise
|
|
||||||
// before performing the explicit bounded poll below.
|
|
||||||
assert!(matches!(
|
|
||||||
poll_once(Pin::as_mut(&mut opening)),
|
|
||||||
Poll::Pending
|
|
||||||
));
|
|
||||||
queue_ready.recv_timeout(Duration::from_secs(2)).unwrap();
|
|
||||||
|
|
||||||
// The valid reply is behind more ignored datagrams than one opening
|
|
||||||
// poll may consume, so this cannot complete without the budget yield.
|
|
||||||
assert!(matches!(
|
|
||||||
poll_once(Pin::as_mut(&mut opening)),
|
|
||||||
Poll::Pending
|
|
||||||
));
|
|
||||||
assert!(matches!(
|
|
||||||
opening.await.unwrap(),
|
|
||||||
OpenOutcome::Started { .. }
|
|
||||||
));
|
|
||||||
});
|
|
||||||
assert_eq!(server.finish().len(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ignored_open_traffic_cannot_extend_the_absolute_attempt_deadline() {
|
fn ignored_open_traffic_cannot_extend_the_absolute_attempt_deadline() {
|
||||||
let server = start_server(move |socket, tx| {
|
let server = start_server(move |socket, tx| {
|
||||||
@@ -485,478 +350,6 @@ fn authenticated_rejection_is_terminal_without_retry() {
|
|||||||
assert_eq!(server.finish().len(), 1);
|
assert_eq!(server.finish().len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn opening_cancellation_before_submission_is_transactional() {
|
|
||||||
let server = start_server(move |socket, tx| {
|
|
||||||
socket
|
|
||||||
.set_read_timeout(Some(Duration::from_millis(50)))
|
|
||||||
.unwrap();
|
|
||||||
let mut buffer = [0_u8; 512];
|
|
||||||
if let Ok((len, _)) = socket.recv_from(&mut buffer) {
|
|
||||||
tx.send(buffer[..len].to_vec()).unwrap();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
runtime().block_on(async {
|
|
||||||
let mut client = AsyncClient::connect(config(server.addr, None, 0))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
client.test_hooks.pause_open_before_writable.set(true);
|
|
||||||
let mut open = Box::pin(client.open());
|
|
||||||
assert!(matches!(poll_once(Pin::as_mut(&mut open)), Poll::Pending));
|
|
||||||
drop(open);
|
|
||||||
|
|
||||||
assert!(client.machine.prepare_open_request().is_ok());
|
|
||||||
assert!(client.prepared_open.is_some());
|
|
||||||
assert!(client.schedule.is_none());
|
|
||||||
assert_eq!(client.applied_traffic_class, None);
|
|
||||||
});
|
|
||||||
assert!(server.finish().is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn opening_cancellation_after_submission_can_retry() {
|
|
||||||
let server = start_server(move |socket, tx| {
|
|
||||||
let (first, _) = recv_packet(&socket, &tx);
|
|
||||||
let (first_params, _) = open_request(&first, None);
|
|
||||||
let (_, peer) = recv_packet(&socket, &tx);
|
|
||||||
send_open_reply(
|
|
||||||
&socket,
|
|
||||||
peer,
|
|
||||||
first_params,
|
|
||||||
None,
|
|
||||||
flags::FLAG_OPEN | flags::FLAG_REPLY,
|
|
||||||
TOKEN,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
runtime().block_on(async {
|
|
||||||
let mut client = AsyncClient::connect(config(server.addr, None, 0))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
client.socket.writable().await.unwrap();
|
|
||||||
client.test_hooks.pause_open_before_readable.set(true);
|
|
||||||
let mut open = Box::pin(client.open());
|
|
||||||
assert!(matches!(poll_once(Pin::as_mut(&mut open)), Poll::Pending));
|
|
||||||
drop(open);
|
|
||||||
|
|
||||||
assert!(client.machine.prepare_open_request().is_ok());
|
|
||||||
assert!(client.prepared_open.is_some());
|
|
||||||
assert!(client.schedule.is_none());
|
|
||||||
assert_eq!(client.applied_traffic_class, None);
|
|
||||||
assert!(matches!(
|
|
||||||
client.open().await.unwrap(),
|
|
||||||
OpenOutcome::Started { .. }
|
|
||||||
));
|
|
||||||
});
|
|
||||||
assert_eq!(server.finish().len(), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn adapter_failure_sends_preencoded_cleanup_without_committing_open() {
|
|
||||||
let server = start_server(move |socket, tx| {
|
|
||||||
open_session(&socket, &tx, None);
|
|
||||||
let (close, _) = recv_packet(&socket, &tx);
|
|
||||||
assert_eq!(close_request_token(&close, None), TOKEN);
|
|
||||||
});
|
|
||||||
|
|
||||||
runtime().block_on(async {
|
|
||||||
let mut client = AsyncClient::connect(config(server.addr, None, 46))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
client.test_hooks.fail_open_dscp.set(true);
|
|
||||||
assert!(matches!(
|
|
||||||
client.open().await,
|
|
||||||
Err(ClientError::SocketOption { .. })
|
|
||||||
));
|
|
||||||
assert!(client.machine.prepare_open_request().is_ok());
|
|
||||||
assert!(client.schedule.is_none());
|
|
||||||
assert_eq!(client.applied_traffic_class, None);
|
|
||||||
});
|
|
||||||
assert_eq!(server.finish().len(), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cleanup_failure_preserves_primary_open_error() {
|
|
||||||
let server = start_server(move |socket, tx| {
|
|
||||||
open_session(&socket, &tx, None);
|
|
||||||
});
|
|
||||||
|
|
||||||
runtime().block_on(async {
|
|
||||||
let mut client = AsyncClient::connect(config(server.addr, None, 46))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
client.test_hooks.fail_open_dscp.set(true);
|
|
||||||
client.test_hooks.fail_cleanup_send.set(true);
|
|
||||||
assert!(matches!(
|
|
||||||
client.open().await,
|
|
||||||
Err(ClientError::SocketOption {
|
|
||||||
operation: "set negotiated DSCP",
|
|
||||||
..
|
|
||||||
})
|
|
||||||
));
|
|
||||||
assert!(client.machine.prepare_open_request().is_ok());
|
|
||||||
});
|
|
||||||
assert_eq!(server.finish().len(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn no_test_open_has_no_schedule_or_dscp_state() {
|
|
||||||
let server = start_no_test_server();
|
|
||||||
runtime().block_on(async {
|
|
||||||
let mut client_config = config(server.addr, None, 46);
|
|
||||||
client_config.run_mode = RunMode::NoTest;
|
|
||||||
let mut client = AsyncClient::connect(client_config).await.unwrap();
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
client.open().await.unwrap(),
|
|
||||||
OpenOutcome::NoTestCompleted { .. }
|
|
||||||
));
|
|
||||||
assert!(client.prepared_open.is_none());
|
|
||||||
assert!(client.schedule.is_none());
|
|
||||||
assert_eq!(client.applied_traffic_class, None);
|
|
||||||
assert!(client.is_run_complete());
|
|
||||||
assert!(matches!(
|
|
||||||
client.close().await,
|
|
||||||
Err(ClientError::AlreadyCompleted)
|
|
||||||
));
|
|
||||||
});
|
|
||||||
assert_eq!(server.finish().len(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn probe_cancellation_would_block_and_error_retain_one_preparation() {
|
|
||||||
let server = start_open_server(None, 1);
|
|
||||||
runtime().block_on(async {
|
|
||||||
let mut client = opened_client(server.addr, 0).await;
|
|
||||||
let deadline = client.next_send_deadline();
|
|
||||||
let attempts = client.test_hooks.send_attempts.get();
|
|
||||||
|
|
||||||
client.test_hooks.pause_probe_before_writable.set(true);
|
|
||||||
let mut send = Box::pin(client.send_probe());
|
|
||||||
assert!(matches!(poll_once(Pin::as_mut(&mut send)), Poll::Pending));
|
|
||||||
drop(send);
|
|
||||||
|
|
||||||
let retained = client.prepared_probe.as_ref().unwrap().bytes.clone();
|
|
||||||
let retained_ptr = client.prepared_probe.as_ref().unwrap().bytes.as_ptr();
|
|
||||||
assert_eq!(client.machine.packets_sent(), 0);
|
|
||||||
assert!(client.machine.pending_is_empty());
|
|
||||||
assert_eq!(client.next_send_deadline(), deadline);
|
|
||||||
assert_eq!(client.test_hooks.send_attempts.get(), attempts);
|
|
||||||
|
|
||||||
client
|
|
||||||
.test_hooks
|
|
||||||
.sends
|
|
||||||
.borrow_mut()
|
|
||||||
.extend([InjectedSend::WouldBlock, InjectedSend::Error]);
|
|
||||||
assert!(matches!(
|
|
||||||
client.send_probe().await,
|
|
||||||
Err(ClientError::Socket(_))
|
|
||||||
));
|
|
||||||
assert_eq!(client.prepared_probe.as_ref().unwrap().bytes, retained);
|
|
||||||
assert_eq!(
|
|
||||||
client.prepared_probe.as_ref().unwrap().bytes.as_ptr(),
|
|
||||||
retained_ptr
|
|
||||||
);
|
|
||||||
assert_eq!(client.machine.packets_sent(), 0);
|
|
||||||
assert!(client.machine.pending_is_empty());
|
|
||||||
assert_eq!(client.next_send_deadline(), deadline);
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
client.send_probe().await.unwrap().as_slice(),
|
|
||||||
[ClientEvent::EchoSent { seq: 0, .. }]
|
|
||||||
));
|
|
||||||
assert_eq!(client.machine.packets_sent(), 1);
|
|
||||||
assert!(client.prepared_probe.is_none());
|
|
||||||
});
|
|
||||||
assert_eq!(server.finish().len(), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn probe_false_readiness_resamples_all_timestamps() {
|
|
||||||
let server = start_open_server(None, 1);
|
|
||||||
runtime().block_on(async {
|
|
||||||
let mut client = opened_client(server.addr, 0).await;
|
|
||||||
let scheduled_at = client.next_send_deadline().unwrap();
|
|
||||||
let first_sent = ClientTimestamp {
|
|
||||||
wall: SystemTime::UNIX_EPOCH + Duration::from_secs(1),
|
|
||||||
mono: scheduled_at + Duration::from_millis(3),
|
|
||||||
};
|
|
||||||
let second_sent = ClientTimestamp {
|
|
||||||
wall: SystemTime::UNIX_EPOCH + Duration::from_secs(2),
|
|
||||||
mono: scheduled_at + Duration::from_millis(11),
|
|
||||||
};
|
|
||||||
client.test_hooks.probe_timestamps.borrow_mut().extend([
|
|
||||||
ProbeSendTimestamps {
|
|
||||||
permission_at: scheduled_at + Duration::from_millis(1),
|
|
||||||
send_anchor: first_sent,
|
|
||||||
sent_at: first_sent,
|
|
||||||
send_call_start: scheduled_at + Duration::from_millis(4),
|
|
||||||
send_finished_at: scheduled_at + Duration::from_millis(6),
|
|
||||||
},
|
|
||||||
ProbeSendTimestamps {
|
|
||||||
permission_at: scheduled_at + Duration::from_millis(8),
|
|
||||||
send_anchor: second_sent,
|
|
||||||
sent_at: second_sent,
|
|
||||||
send_call_start: scheduled_at + Duration::from_millis(12),
|
|
||||||
send_finished_at: scheduled_at + Duration::from_millis(17),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
inject_send(&client, InjectedSend::WouldBlock);
|
|
||||||
|
|
||||||
let events = client.send_probe().await.unwrap();
|
|
||||||
assert!(matches!(
|
|
||||||
events.as_slice(),
|
|
||||||
[ClientEvent::EchoSent {
|
|
||||||
scheduled_at: event_scheduled,
|
|
||||||
sent_at,
|
|
||||||
send_call,
|
|
||||||
timer_error,
|
|
||||||
..
|
|
||||||
}] if *event_scheduled == scheduled_at
|
|
||||||
&& *sent_at == second_sent
|
|
||||||
&& *send_call == Duration::from_millis(5)
|
|
||||||
&& *timer_error == Duration::from_millis(11)
|
|
||||||
));
|
|
||||||
assert!(matches!(
|
|
||||||
client
|
|
||||||
.poll_timeouts_at(second_sent.mono + client.probe_timeout())
|
|
||||||
.unwrap()
|
|
||||||
.as_slice(),
|
|
||||||
[ClientEvent::EchoLoss { sent_at, .. }] if *sent_at == second_sent
|
|
||||||
));
|
|
||||||
});
|
|
||||||
assert_eq!(server.finish().len(), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn public_timeout_polling_remains_exhaustive() {
|
|
||||||
let server = start_open_server(None, 0);
|
|
||||||
runtime().block_on(async {
|
|
||||||
let mut client = opened_client(server.addr, 0).await;
|
|
||||||
let now = Instant::now();
|
|
||||||
for seq in 0..3 {
|
|
||||||
client.replace_pending_for_test(PendingProbe {
|
|
||||||
wire_seq: seq,
|
|
||||||
sent_at: ClientTimestamp {
|
|
||||||
mono: now - Duration::from_secs(2),
|
|
||||||
wall: SystemTime::UNIX_EPOCH,
|
|
||||||
},
|
|
||||||
timeout_at: now - Duration::from_secs(1),
|
|
||||||
tx_not_before_wall: SystemTime::UNIX_EPOCH,
|
|
||||||
kernel_tx_timestamp: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
client.poll_timeouts_at(now).unwrap().as_slice(),
|
|
||||||
[
|
|
||||||
ClientEvent::EchoLoss { seq: 0, .. },
|
|
||||||
ClientEvent::EchoLoss { seq: 1, .. },
|
|
||||||
ClientEvent::EchoLoss { seq: 2, .. },
|
|
||||||
]
|
|
||||||
));
|
|
||||||
});
|
|
||||||
assert_eq!(server.finish().len(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn successful_short_probe_commits_before_length_error() {
|
|
||||||
let server = start_open_server(None, 0);
|
|
||||||
runtime().block_on(async {
|
|
||||||
let mut client = opened_client(server.addr, 0).await;
|
|
||||||
client.test_hooks.pause_probe_before_writable.set(true);
|
|
||||||
let mut send = Box::pin(client.send_probe());
|
|
||||||
assert!(matches!(poll_once(Pin::as_mut(&mut send)), Poll::Pending));
|
|
||||||
drop(send);
|
|
||||||
let probe_len = client.prepared_probe.as_ref().unwrap().bytes.len();
|
|
||||||
let deadline = client.next_send_deadline().unwrap();
|
|
||||||
inject_send(&client, InjectedSend::ReportedLength(probe_len - 1));
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
client.send_probe().await,
|
|
||||||
Err(ClientError::DatagramLengthMismatch { .. })
|
|
||||||
));
|
|
||||||
assert_eq!(client.machine.packets_sent(), 1);
|
|
||||||
assert!(!client.machine.pending_is_empty());
|
|
||||||
assert!(client.prepared_probe.is_none());
|
|
||||||
assert_eq!(
|
|
||||||
client.next_send_deadline(),
|
|
||||||
Some(deadline + client.machine.config().interval)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
assert_eq!(server.finish().len(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn stale_retained_probe_is_rejected_before_send() {
|
|
||||||
let server = start_open_server(None, 0);
|
|
||||||
runtime().block_on(async {
|
|
||||||
let mut client = opened_client(server.addr, 0).await;
|
|
||||||
client.test_hooks.pause_probe_before_writable.set(true);
|
|
||||||
let mut send = Box::pin(client.send_probe());
|
|
||||||
assert!(matches!(poll_once(Pin::as_mut(&mut send)), Poll::Pending));
|
|
||||||
drop(send);
|
|
||||||
|
|
||||||
let advanced = client.machine.prepare_probe().unwrap().unwrap();
|
|
||||||
let preflight = client.machine.preflight_probe_commit(&advanced).unwrap();
|
|
||||||
let commit = client
|
|
||||||
.machine
|
|
||||||
.finalize_probe_commit(preflight, ClientTimestamp::now())
|
|
||||||
.unwrap();
|
|
||||||
client
|
|
||||||
.machine
|
|
||||||
.commit_probe_sent(commit, ClientTimestamp::now(), advanced.bytes.len());
|
|
||||||
let attempts = client.test_hooks.send_attempts.get();
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
client.send_probe().await,
|
|
||||||
Err(ClientError::StalePreparedProbe { .. })
|
|
||||||
));
|
|
||||||
assert_eq!(client.test_hooks.send_attempts.get(), attempts);
|
|
||||||
assert!(client.prepared_probe.is_some());
|
|
||||||
});
|
|
||||||
assert_eq!(server.finish().len(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn finite_duration_boundary_clears_preparation_without_send() {
|
|
||||||
let server = start_open_server(None, 0);
|
|
||||||
runtime().block_on(async {
|
|
||||||
let mut client_config = config(server.addr, None, 0);
|
|
||||||
client_config.duration = Some(Duration::from_millis(20));
|
|
||||||
let mut client = AsyncClient::connect(client_config).await.unwrap();
|
|
||||||
client.open().await.unwrap();
|
|
||||||
let start = client.next_send_deadline().unwrap();
|
|
||||||
let attempts = client.test_hooks.send_attempts.get();
|
|
||||||
client
|
|
||||||
.test_hooks
|
|
||||||
.probe_timestamps
|
|
||||||
.borrow_mut()
|
|
||||||
.push_back(ProbeSendTimestamps {
|
|
||||||
permission_at: start + Duration::from_millis(20),
|
|
||||||
send_anchor: ClientTimestamp {
|
|
||||||
wall: SystemTime::UNIX_EPOCH,
|
|
||||||
mono: start + Duration::from_millis(21),
|
|
||||||
},
|
|
||||||
sent_at: ClientTimestamp {
|
|
||||||
wall: SystemTime::UNIX_EPOCH,
|
|
||||||
mono: start + Duration::from_millis(21),
|
|
||||||
},
|
|
||||||
send_call_start: start + Duration::from_millis(22),
|
|
||||||
send_finished_at: start + Duration::from_millis(23),
|
|
||||||
});
|
|
||||||
|
|
||||||
assert!(client.send_probe().await.unwrap().is_empty());
|
|
||||||
assert!(client.prepared_probe.is_none());
|
|
||||||
assert_eq!(client.machine.packets_sent(), 0);
|
|
||||||
assert_eq!(client.test_hooks.send_attempts.get(), attempts);
|
|
||||||
assert!(client.is_run_complete());
|
|
||||||
});
|
|
||||||
assert_eq!(server.finish().len(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn receive_retries_false_readiness_and_shares_packet_classification() {
|
|
||||||
let server = start_server(move |socket, tx| {
|
|
||||||
let (params, peer) = open_session(&socket, &tx, None);
|
|
||||||
let (probe_packet, _) = recv_packet(&socket, &tx);
|
|
||||||
let sequence = echo_request_sequence(&probe_packet, None);
|
|
||||||
socket
|
|
||||||
.send_to(
|
|
||||||
&echo_reply(¶ms, sequence, TOKEN, flags::FLAG_REPLY, None),
|
|
||||||
peer,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
runtime().block_on(async {
|
|
||||||
let mut client = opened_client(server.addr, 0).await;
|
|
||||||
client.send_probe().await.unwrap();
|
|
||||||
client.test_hooks.receive_would_block.set(2);
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
client.recv().await.unwrap().as_slice(),
|
|
||||||
[ClientEvent::EchoReply { seq: 0, .. }]
|
|
||||||
));
|
|
||||||
});
|
|
||||||
assert_eq!(server.finish().len(), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn receive_retries_interrupted_syscall_and_shares_packet_classification() {
|
|
||||||
let server = start_server(move |socket, tx| {
|
|
||||||
let (params, peer) = open_session(&socket, &tx, None);
|
|
||||||
let (probe_packet, _) = recv_packet(&socket, &tx);
|
|
||||||
let sequence = echo_request_sequence(&probe_packet, None);
|
|
||||||
socket
|
|
||||||
.send_to(
|
|
||||||
&echo_reply(¶ms, sequence, TOKEN, flags::FLAG_REPLY, None),
|
|
||||||
peer,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
runtime().block_on(async {
|
|
||||||
let mut client = opened_client(server.addr, 0).await;
|
|
||||||
client.send_probe().await.unwrap();
|
|
||||||
client.test_hooks.inject_recv_interrupted.set(2);
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
client.recv().await.unwrap().as_slice(),
|
|
||||||
[ClientEvent::EchoReply { seq: 0, .. }]
|
|
||||||
));
|
|
||||||
});
|
|
||||||
assert_eq!(server.finish().len(), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn open_retries_interrupted_syscall_within_the_same_attempt() {
|
|
||||||
let server = start_open_server(None, 0);
|
|
||||||
runtime().block_on(async {
|
|
||||||
let mut client = AsyncClient::connect(config(server.addr, None, 0))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
client.test_hooks.inject_recv_interrupted.set(3);
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
client.open().await,
|
|
||||||
Ok(OpenOutcome::Started { .. })
|
|
||||||
));
|
|
||||||
// Only one open request reached the server: interruptions were
|
|
||||||
// absorbed inside the single open attempt, with no retransmit.
|
|
||||||
assert_eq!(client.test_hooks.send_attempts.get(), 1);
|
|
||||||
});
|
|
||||||
server.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn peer_close_dscp_failure_preserves_events_and_logical_ownership() {
|
|
||||||
let server = start_peer_close_server();
|
|
||||||
runtime().block_on(async {
|
|
||||||
let mut client = opened_client(server.addr, 46).await;
|
|
||||||
client.send_probe().await.unwrap();
|
|
||||||
client.prepared_probe = client.machine.prepare_probe().unwrap();
|
|
||||||
client.test_hooks.fail_peer_close_dscp.set(true);
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
client.recv().await.unwrap().as_slice(),
|
|
||||||
[
|
|
||||||
ClientEvent::EchoReply { .. },
|
|
||||||
ClientEvent::SessionClosed { .. }
|
|
||||||
]
|
|
||||||
));
|
|
||||||
assert!(client.is_peer_closed());
|
|
||||||
assert!(client.is_run_complete());
|
|
||||||
assert!(client.schedule.is_none());
|
|
||||||
assert!(client.prepared_probe.is_none());
|
|
||||||
assert_eq!(client.applied_traffic_class, Some(184));
|
|
||||||
assert_eq!(client.next_send_deadline(), None);
|
|
||||||
assert_eq!(socket_dscp(&client), 46);
|
|
||||||
});
|
|
||||||
assert_eq!(server.finish().len(), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn recv_after_local_close_and_no_test_fails_on_first_poll() {
|
fn recv_after_local_close_and_no_test_fails_on_first_poll() {
|
||||||
let close_server = start_server(move |socket, tx| {
|
let close_server = start_server(move |socket, tx| {
|
||||||
@@ -988,141 +381,6 @@ fn recv_after_local_close_and_no_test_fails_on_first_poll() {
|
|||||||
assert_eq!(no_test_server.finish().len(), 1);
|
assert_eq!(no_test_server.finish().len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn close_cancellation_and_would_block_restore_dscp_before_suspend() {
|
|
||||||
let server = start_open_server(None, 1);
|
|
||||||
runtime().block_on(async {
|
|
||||||
let mut client = opened_client(server.addr, 46).await;
|
|
||||||
let deadline = client.next_send_deadline();
|
|
||||||
let attempts = client.test_hooks.send_attempts.get();
|
|
||||||
assert_eq!(socket_dscp(&client), 46);
|
|
||||||
|
|
||||||
client.test_hooks.pause_close_before_writable.set(true);
|
|
||||||
let mut close = Box::pin(client.close());
|
|
||||||
assert!(matches!(poll_once(Pin::as_mut(&mut close)), Poll::Pending));
|
|
||||||
drop(close);
|
|
||||||
assert!(client.machine.is_open());
|
|
||||||
assert!(client.schedule.is_some());
|
|
||||||
assert_eq!(client.next_send_deadline(), deadline);
|
|
||||||
assert_eq!(client.applied_traffic_class, Some(184));
|
|
||||||
assert_eq!(client.test_hooks.send_attempts.get(), attempts);
|
|
||||||
assert_eq!(socket_dscp(&client), 46);
|
|
||||||
|
|
||||||
inject_send(&client, InjectedSend::WouldBlock);
|
|
||||||
client.test_hooks.pause_close_after_would_block.set(true);
|
|
||||||
let mut close = Box::pin(client.close());
|
|
||||||
assert!(matches!(poll_once(Pin::as_mut(&mut close)), Poll::Pending));
|
|
||||||
drop(close);
|
|
||||||
assert!(client.machine.is_open());
|
|
||||||
assert!(client.schedule.is_some());
|
|
||||||
assert_eq!(client.applied_traffic_class, Some(184));
|
|
||||||
assert_eq!(socket_dscp(&client), 46);
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
client.close().await.unwrap().as_slice(),
|
|
||||||
[ClientEvent::SessionClosed { .. }]
|
|
||||||
));
|
|
||||||
assert!(client.schedule.is_none());
|
|
||||||
assert_eq!(client.applied_traffic_class, None);
|
|
||||||
});
|
|
||||||
assert_eq!(server.finish().len(), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn close_errors_preserve_open_state_and_primary_send_error() {
|
|
||||||
let server = start_open_server(None, 0);
|
|
||||||
runtime().block_on(async {
|
|
||||||
let mut client = opened_client(server.addr, 46).await;
|
|
||||||
let deadline = client.next_send_deadline();
|
|
||||||
|
|
||||||
inject_send(&client, InjectedSend::Error);
|
|
||||||
assert!(matches!(client.close().await, Err(ClientError::Socket(_))));
|
|
||||||
assert!(client.machine.is_open());
|
|
||||||
assert!(client.schedule.is_some());
|
|
||||||
assert_eq!(client.next_send_deadline(), deadline);
|
|
||||||
assert_eq!(client.applied_traffic_class, Some(184));
|
|
||||||
assert_eq!(socket_dscp(&client), 46);
|
|
||||||
|
|
||||||
client.test_hooks.fail_dscp_restore.set(true);
|
|
||||||
inject_send(&client, InjectedSend::Error);
|
|
||||||
assert!(matches!(client.close().await, Err(ClientError::Socket(_))));
|
|
||||||
assert!(client.machine.is_open());
|
|
||||||
assert!(client.schedule.is_some());
|
|
||||||
});
|
|
||||||
assert_eq!(server.finish().len(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn close_would_block_restore_failure_stops_without_committing() {
|
|
||||||
let server = start_open_server(None, 0);
|
|
||||||
runtime().block_on(async {
|
|
||||||
let mut client = opened_client(server.addr, 46).await;
|
|
||||||
client.test_hooks.fail_dscp_restore.set(true);
|
|
||||||
inject_send(&client, InjectedSend::WouldBlock);
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
client.close().await,
|
|
||||||
Err(ClientError::SocketOption {
|
|
||||||
operation: "restore negotiated DSCP",
|
|
||||||
..
|
|
||||||
})
|
|
||||||
));
|
|
||||||
assert!(client.machine.is_open());
|
|
||||||
assert!(client.schedule.is_some());
|
|
||||||
assert_eq!(client.applied_traffic_class, Some(184));
|
|
||||||
});
|
|
||||||
assert_eq!(server.finish().len(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn successful_short_close_commits_before_length_error() {
|
|
||||||
let server = start_open_server(None, 0);
|
|
||||||
runtime().block_on(async {
|
|
||||||
let mut client = opened_client(server.addr, 0).await;
|
|
||||||
client.test_hooks.pause_probe_before_writable.set(true);
|
|
||||||
let mut send = Box::pin(client.send_probe());
|
|
||||||
assert!(matches!(poll_once(Pin::as_mut(&mut send)), Poll::Pending));
|
|
||||||
drop(send);
|
|
||||||
assert!(client.prepared_probe.is_some());
|
|
||||||
|
|
||||||
let close_len = client.machine.prepare_close().unwrap().bytes.len();
|
|
||||||
inject_send(&client, InjectedSend::ReportedLength(close_len - 1));
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
client.close().await,
|
|
||||||
Err(ClientError::DatagramLengthMismatch { .. })
|
|
||||||
));
|
|
||||||
assert!(!client.machine.is_open());
|
|
||||||
assert!(client.schedule.is_none());
|
|
||||||
assert!(client.prepared_probe.is_none());
|
|
||||||
assert_eq!(client.applied_traffic_class, None);
|
|
||||||
assert!(matches!(
|
|
||||||
client.machine.prepare_close(),
|
|
||||||
Err(ClientError::AlreadyClosed)
|
|
||||||
));
|
|
||||||
});
|
|
||||||
assert_eq!(server.finish().len(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn successful_close_uses_post_acceptance_timestamp() {
|
|
||||||
let server = start_open_server(None, 1);
|
|
||||||
runtime().block_on(async {
|
|
||||||
let mut client = opened_client(server.addr, 0).await;
|
|
||||||
let close_sent_at = ClientTimestamp {
|
|
||||||
wall: SystemTime::UNIX_EPOCH + Duration::from_secs(44),
|
|
||||||
mono: Instant::now() + Duration::from_secs(1),
|
|
||||||
};
|
|
||||||
client.test_hooks.close_sent_at.set(Some(close_sent_at));
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
client.close().await.unwrap().as_slice(),
|
|
||||||
[ClientEvent::SessionClosed { at, .. }] if *at == close_sent_at
|
|
||||||
));
|
|
||||||
});
|
|
||||||
assert_eq!(server.finish().len(), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn blocking_and_async_hmac_dscp_lifecycle_are_semantically_equivalent() {
|
fn blocking_and_async_hmac_dscp_lifecycle_are_semantically_equivalent() {
|
||||||
let key = b"async-equivalence-key".to_vec();
|
let key = b"async-equivalence-key".to_vec();
|
||||||
@@ -1178,7 +436,7 @@ fn blocking_and_async_no_test_are_semantically_equivalent() {
|
|||||||
blocking_no_test_config.run_mode = RunMode::NoTest;
|
blocking_no_test_config.run_mode = RunMode::NoTest;
|
||||||
let mut blocking_no_test = Client::connect(blocking_no_test_config).unwrap();
|
let mut blocking_no_test = Client::connect(blocking_no_test_config).unwrap();
|
||||||
let blocking_no_test_open = blocking_no_test.open().unwrap();
|
let blocking_no_test_open = blocking_no_test.open().unwrap();
|
||||||
assert!(blocking_no_test.is_run_complete());
|
assert!(blocking_no_test.negotiated_params().is_none());
|
||||||
drop(blocking_no_test_server);
|
drop(blocking_no_test_server);
|
||||||
|
|
||||||
let async_no_test_server = InTreeServer::start(ServerConfig::default());
|
let async_no_test_server = InTreeServer::start(ServerConfig::default());
|
||||||
@@ -1187,7 +445,7 @@ fn blocking_and_async_no_test_are_semantically_equivalent() {
|
|||||||
let async_no_test_open = runtime().block_on(async {
|
let async_no_test_open = runtime().block_on(async {
|
||||||
let mut client = AsyncClient::connect(async_no_test_config).await.unwrap();
|
let mut client = AsyncClient::connect(async_no_test_config).await.unwrap();
|
||||||
let opened = client.open().await.unwrap();
|
let opened = client.open().await.unwrap();
|
||||||
assert!(client.is_run_complete());
|
assert!(client.negotiated_params().is_none());
|
||||||
opened
|
opened
|
||||||
});
|
});
|
||||||
drop(async_no_test_server);
|
drop(async_no_test_server);
|
||||||
@@ -1332,8 +590,11 @@ fn blocking_and_async_roll_back_a_failed_open_and_accept_a_retry() {
|
|||||||
// must be able to open the same connected client.
|
// must be able to open the same connected client.
|
||||||
let blocking_server = start_silent_then_open_server(1);
|
let blocking_server = start_silent_then_open_server(1);
|
||||||
let mut blocking = Client::connect(config(blocking_server.addr, None, 0)).unwrap();
|
let mut blocking = Client::connect(config(blocking_server.addr, None, 0)).unwrap();
|
||||||
|
assert!(!blocking.has_pending_probes());
|
||||||
let blocking_first = blocking.open().unwrap_err();
|
let blocking_first = blocking.open().unwrap_err();
|
||||||
|
assert!(!blocking.has_pending_probes());
|
||||||
let blocking_second = blocking.open().unwrap();
|
let blocking_second = blocking.open().unwrap();
|
||||||
|
assert!(!blocking.has_pending_probes());
|
||||||
let blocking_sent = blocking.send_probe().unwrap();
|
let blocking_sent = blocking.send_probe().unwrap();
|
||||||
let blocking_reply = blocking.recv_once().unwrap();
|
let blocking_reply = blocking.recv_once().unwrap();
|
||||||
let blocking_close = blocking.close().unwrap();
|
let blocking_close = blocking.close().unwrap();
|
||||||
@@ -1345,8 +606,11 @@ fn blocking_and_async_roll_back_a_failed_open_and_accept_a_retry() {
|
|||||||
let mut client = AsyncClient::connect(config(async_server.addr, None, 0))
|
let mut client = AsyncClient::connect(config(async_server.addr, None, 0))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
assert!(!client.has_pending_probes());
|
||||||
let first = client.open().await.unwrap_err();
|
let first = client.open().await.unwrap_err();
|
||||||
|
assert!(!client.has_pending_probes());
|
||||||
let second = client.open().await.unwrap();
|
let second = client.open().await.unwrap();
|
||||||
|
assert!(!client.has_pending_probes());
|
||||||
let sent = client.send_probe().await.unwrap();
|
let sent = client.send_probe().await.unwrap();
|
||||||
let reply = client.recv().await.unwrap();
|
let reply = client.recv().await.unwrap();
|
||||||
let closed = client.close().await.unwrap();
|
let closed = client.close().await.unwrap();
|
||||||
@@ -1380,30 +644,53 @@ fn blocking_and_async_roll_back_a_failed_open_and_accept_a_retry() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn blocking_and_async_complete_every_caller_paced_probe() {
|
fn blocking_and_async_complete_every_caller_paced_probe() {
|
||||||
const PROBES: u32 = 3;
|
const PROBES: u32 = 3;
|
||||||
|
fn caller_config(addr: SocketAddr) -> ClientConfig {
|
||||||
|
ClientConfig {
|
||||||
|
duration: Some(Duration::from_millis(1)),
|
||||||
|
interval: Duration::from_secs(1),
|
||||||
|
..config(addr, None, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let blocking_server = InTreeServer::start(ServerConfig::default());
|
let blocking_server = InTreeServer::start(ServerConfig::default());
|
||||||
let mut blocking = Client::connect(config(blocking_server.addr, None, 0)).unwrap();
|
let mut blocking = Client::connect(caller_config(blocking_server.addr)).unwrap();
|
||||||
blocking.open().unwrap();
|
let outcome = blocking.open().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
blocking.negotiated_params(),
|
||||||
|
Some(open_negotiated(&outcome))
|
||||||
|
);
|
||||||
|
assert!(!blocking.has_pending_probes());
|
||||||
|
// Oversleeping cannot invalidate this: low-level sends remain caller-owned
|
||||||
|
// after the negotiated duration. The compliant server has no duration cap.
|
||||||
|
thread::sleep(Duration::from_millis(2));
|
||||||
let mut blocking_sent = Vec::new();
|
let mut blocking_sent = Vec::new();
|
||||||
let mut blocking_replies = Vec::new();
|
let mut blocking_replies = Vec::new();
|
||||||
for _ in 0..PROBES {
|
for _ in 0..PROBES {
|
||||||
blocking_sent.extend(blocking.send_probe().unwrap());
|
blocking_sent.extend(blocking.send_probe().unwrap());
|
||||||
|
assert!(blocking.has_pending_probes());
|
||||||
|
assert!(blocking.next_probe_timeout_deadline().is_some());
|
||||||
blocking_replies.extend(blocking.recv_once().unwrap());
|
blocking_replies.extend(blocking.recv_once().unwrap());
|
||||||
|
assert!(!blocking.has_pending_probes());
|
||||||
}
|
}
|
||||||
blocking.close().unwrap();
|
blocking.close().unwrap();
|
||||||
drop(blocking_server);
|
drop(blocking_server);
|
||||||
|
|
||||||
let async_server = InTreeServer::start(ServerConfig::default());
|
let async_server = InTreeServer::start(ServerConfig::default());
|
||||||
let (async_sent, async_replies) = runtime().block_on(async {
|
let (async_sent, async_replies) = runtime().block_on(async {
|
||||||
let mut client = AsyncClient::connect(config(async_server.addr, None, 0))
|
let mut client = AsyncClient::connect(caller_config(async_server.addr))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
client.open().await.unwrap();
|
let outcome = client.open().await.unwrap();
|
||||||
|
assert_eq!(client.negotiated_params(), Some(open_negotiated(&outcome)));
|
||||||
|
tokio::time::sleep(Duration::from_millis(2)).await;
|
||||||
let mut sent = Vec::new();
|
let mut sent = Vec::new();
|
||||||
let mut replies = Vec::new();
|
let mut replies = Vec::new();
|
||||||
for _ in 0..PROBES {
|
for _ in 0..PROBES {
|
||||||
sent.extend(client.send_probe().await.unwrap());
|
sent.extend(client.send_probe().await.unwrap());
|
||||||
|
assert!(client.has_pending_probes());
|
||||||
|
assert!(client.next_probe_timeout_deadline().is_some());
|
||||||
replies.extend(client.recv().await.unwrap());
|
replies.extend(client.recv().await.unwrap());
|
||||||
|
assert!(!client.has_pending_probes());
|
||||||
}
|
}
|
||||||
client.close().await.unwrap();
|
client.close().await.unwrap();
|
||||||
(sent, replies)
|
(sent, replies)
|
||||||
@@ -1419,6 +706,16 @@ fn blocking_and_async_complete_every_caller_paced_probe() {
|
|||||||
// the sequence helpers keep: an extra event on either side is a difference.
|
// the sequence helpers keep: an extra event on either side is a difference.
|
||||||
assert_eq!(blocking_sent.len(), async_sent.len());
|
assert_eq!(blocking_sent.len(), async_sent.len());
|
||||||
assert_eq!(blocking_replies.len(), async_replies.len());
|
assert_eq!(blocking_replies.len(), async_replies.len());
|
||||||
|
for event in blocking_sent.iter().chain(&async_sent) {
|
||||||
|
assert!(matches!(
|
||||||
|
event,
|
||||||
|
ClientEvent::EchoSent {
|
||||||
|
scheduled_at: None,
|
||||||
|
timer_error: None,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
for (blocking, asynchronous) in blocking_sent.iter().zip(&async_sent) {
|
for (blocking, asynchronous) in blocking_sent.iter().zip(&async_sent) {
|
||||||
assert_matching_event_shape(blocking, asynchronous);
|
assert_matching_event_shape(blocking, asynchronous);
|
||||||
}
|
}
|
||||||
@@ -1470,3 +767,24 @@ fn blocking_and_async_reject_probes_before_open_and_after_close() {
|
|||||||
error_name(&async_after_close)
|
error_name(&async_after_close)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ignored_open_traffic_uses_one_request_and_deadline_per_attempt() {
|
||||||
|
let server = start_server(move |socket, tx| {
|
||||||
|
for _ in 0..2 {
|
||||||
|
let (_, peer) = recv_packet(&socket, &tx);
|
||||||
|
for _ in 0..3 {
|
||||||
|
socket.send_to(&[0_u8], peer).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let mut client_config = config(server.addr, None, 0);
|
||||||
|
client_config.open_timeouts = vec![Duration::from_millis(200), Duration::from_millis(200)];
|
||||||
|
|
||||||
|
runtime().block_on(async {
|
||||||
|
let mut client = AsyncClient::connect(client_config).await.unwrap();
|
||||||
|
assert!(matches!(client.open().await, Err(ClientError::OpenTimeout)));
|
||||||
|
assert!(client.machine.prepare_open_request().is_ok());
|
||||||
|
});
|
||||||
|
assert_eq!(server.finish().len(), 2);
|
||||||
|
}
|
||||||
@@ -4,12 +4,6 @@ use std::{
|
|||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
use std::cell::Cell;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
use irtt_proto::{flags, Params, TimestampFields, PROTOCOL_VERSION};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{ClientConfig, RecvBudget},
|
config::{ClientConfig, RecvBudget},
|
||||||
error::ClientError,
|
error::ClientError,
|
||||||
@@ -24,23 +18,9 @@ use crate::{
|
|||||||
timing::ClientTimestamp,
|
timing::ClientTimestamp,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) mod schedule;
|
|
||||||
|
|
||||||
use schedule::{instant_abs_diff, ProbeSchedule};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
enum ProbeScheduleMode {
|
|
||||||
CallerPaced,
|
|
||||||
#[cfg(test)]
|
|
||||||
Managed {
|
|
||||||
scheduled_at: Instant,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct PreparedClientOpen {
|
struct PreparedClientOpen {
|
||||||
machine: PreparedOpenAcceptance,
|
machine: PreparedOpenAcceptance,
|
||||||
schedule: Option<ProbeSchedule>,
|
|
||||||
recv_buffer_len: Option<usize>,
|
recv_buffer_len: Option<usize>,
|
||||||
negotiated_traffic_class: Option<u8>,
|
negotiated_traffic_class: Option<u8>,
|
||||||
previous_traffic_class: Option<u8>,
|
previous_traffic_class: Option<u8>,
|
||||||
@@ -53,50 +33,6 @@ struct PreparedClientOpenFailure {
|
|||||||
machine: PreparedOpenAcceptance,
|
machine: PreparedOpenAcceptance,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
struct ClientTestHooks {
|
|
||||||
fail_open_dscp: Cell<bool>,
|
|
||||||
fail_open_timeout_restore: Cell<bool>,
|
|
||||||
fail_close_event_reserve: Cell<bool>,
|
|
||||||
fail_close_dscp_clear: Cell<bool>,
|
|
||||||
fail_close_send: Cell<bool>,
|
|
||||||
close_reported_len: Cell<Option<usize>>,
|
|
||||||
close_send_attempts: Cell<usize>,
|
|
||||||
close_sent_at: Cell<Option<ClientTimestamp>>,
|
|
||||||
fail_cleanup_send: Cell<bool>,
|
|
||||||
recv_buffer_len_override: Cell<Option<usize>>,
|
|
||||||
fail_dscp_restore: Cell<bool>,
|
|
||||||
last_restored_read_timeout: Cell<Option<Duration>>,
|
|
||||||
prepared_active_session_before_dscp: Cell<bool>,
|
|
||||||
/// Number of subsequent receive attempts that should report
|
|
||||||
/// `io::ErrorKind::Interrupted` before a real receive is attempted.
|
|
||||||
/// Decremented on every injected receive; `0` means inject nothing.
|
|
||||||
inject_recv_interrupted: Cell<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub(crate) struct ProbeSendTimestamps {
|
|
||||||
pub(crate) permission_at: Instant,
|
|
||||||
/// Private pre-send anchor: timeout deadline / kernel TX lower bound.
|
|
||||||
pub(crate) send_anchor: ClientTimestamp,
|
|
||||||
pub(crate) send_call_start: Instant,
|
|
||||||
pub(crate) send_finished_at: Instant,
|
|
||||||
/// Public post-send measurement: RTT / OWD fallback / timer_error.
|
|
||||||
pub(crate) sent_at: ClientTimestamp,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
use crate::{
|
|
||||||
session::machine::{
|
|
||||||
compute_one_way, compute_rtt, params_from_config, sequence_is_after, sequence_is_before,
|
|
||||||
unix_epoch_ns_i64, update_highest_received,
|
|
||||||
},
|
|
||||||
session::negotiate_params,
|
|
||||||
NegotiatedParams, RunMode, SignedDuration, WarningKind, MAX_UDP_PAYLOAD_LENGTH,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Low-level synchronous IRTT client.
|
/// Low-level synchronous IRTT client.
|
||||||
///
|
///
|
||||||
/// `Client` exposes the protocol steps directly: connect a UDP socket, open a
|
/// `Client` exposes the protocol steps directly: connect a UDP socket, open a
|
||||||
@@ -131,7 +67,6 @@ use crate::{
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
runtime: SessionMachine,
|
runtime: SessionMachine,
|
||||||
schedule: Option<ProbeSchedule>,
|
|
||||||
socket: UdpSocket,
|
socket: UdpSocket,
|
||||||
remote: SocketAddr,
|
remote: SocketAddr,
|
||||||
recv_buffer: Vec<u8>,
|
recv_buffer: Vec<u8>,
|
||||||
@@ -143,14 +78,6 @@ pub struct Client {
|
|||||||
/// unnecessary error-queue drains are skipped once it is known there is
|
/// unnecessary error-queue drains are skipped once it is known there is
|
||||||
/// nothing to drain.
|
/// nothing to drain.
|
||||||
tx_timestamping_enabled: bool,
|
tx_timestamping_enabled: bool,
|
||||||
#[cfg(test)]
|
|
||||||
test_hooks: ClientTestHooks,
|
|
||||||
#[cfg(test)]
|
|
||||||
probe_reported_len: Option<usize>,
|
|
||||||
#[cfg(test)]
|
|
||||||
probe_send_error: bool,
|
|
||||||
#[cfg(test)]
|
|
||||||
probe_send_timestamps: Option<ProbeSendTimestamps>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
@@ -167,20 +94,11 @@ impl Client {
|
|||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
runtime,
|
runtime,
|
||||||
schedule: None,
|
|
||||||
socket,
|
socket,
|
||||||
remote,
|
remote,
|
||||||
recv_buffer: vec![0_u8; recv_buffer_size(false, None)?],
|
recv_buffer: vec![0_u8; recv_buffer_size(false, None)?],
|
||||||
applied_traffic_class: None,
|
applied_traffic_class: None,
|
||||||
tx_timestamping_enabled: false,
|
tx_timestamping_enabled: false,
|
||||||
#[cfg(test)]
|
|
||||||
test_hooks: ClientTestHooks::default(),
|
|
||||||
#[cfg(test)]
|
|
||||||
probe_reported_len: None,
|
|
||||||
#[cfg(test)]
|
|
||||||
probe_send_error: false,
|
|
||||||
#[cfg(test)]
|
|
||||||
probe_send_timestamps: None,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,8 +121,9 @@ impl Client {
|
|||||||
pub fn open(&mut self) -> Result<OpenOutcome, ClientError> {
|
pub fn open(&mut self) -> Result<OpenOutcome, ClientError> {
|
||||||
let result = self.open_transaction();
|
let result = self.open_transaction();
|
||||||
if result.is_err() {
|
if result.is_err() {
|
||||||
let _ =
|
let _ = self
|
||||||
self.restore_open_read_timeout(self.runtime.config().socket_config.recv_timeout);
|
.socket
|
||||||
|
.set_read_timeout(self.runtime.config().socket_config.recv_timeout);
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
@@ -214,19 +133,11 @@ impl Client {
|
|||||||
/// The close event means the client has sent its close packet and stopped
|
/// The close event means the client has sent its close packet and stopped
|
||||||
/// tracking the session locally; it is not a server acknowledgement. If the
|
/// tracking the session locally; it is not a server acknowledgement. If the
|
||||||
/// send fails, the negotiated DSCP is restored best-effort and the session
|
/// send fails, the negotiated DSCP is restored best-effort and the session
|
||||||
/// and probe schedule remain open.
|
/// remains open.
|
||||||
pub fn close(&mut self) -> Result<Vec<ClientEvent>, ClientError> {
|
pub fn close(&mut self) -> Result<Vec<ClientEvent>, ClientError> {
|
||||||
let prepared = self.runtime.prepare_close()?;
|
let prepared = self.runtime.prepare_close()?;
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
#[cfg(test)]
|
|
||||||
if self.test_hooks.fail_close_event_reserve.replace(false) {
|
|
||||||
events
|
|
||||||
.try_reserve(usize::MAX)
|
|
||||||
.map_err(|source| ClientError::AllocationFailed {
|
|
||||||
operation: "close event result",
|
|
||||||
source,
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
events
|
events
|
||||||
.try_reserve(1)
|
.try_reserve(1)
|
||||||
.map_err(|source| ClientError::AllocationFailed {
|
.map_err(|source| ClientError::AllocationFailed {
|
||||||
@@ -237,51 +148,74 @@ impl Client {
|
|||||||
let expected_bytes = prepared.bytes.len();
|
let expected_bytes = prepared.bytes.len();
|
||||||
|
|
||||||
self.clear_close_dscp()?;
|
self.clear_close_dscp()?;
|
||||||
let bytes = match self.send_close_datagram(prepared.bytes) {
|
let bytes = match self.socket.send(prepared.bytes) {
|
||||||
Ok(bytes) => bytes,
|
Ok(bytes) => bytes,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.restore_dscp_best_effort(previous_traffic_class);
|
self.restore_dscp_best_effort(previous_traffic_class);
|
||||||
return Err(ClientError::Socket(err));
|
return Err(ClientError::Socket(err));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
#[cfg(not(test))]
|
|
||||||
let sent_at = ClientTimestamp::now();
|
let sent_at = ClientTimestamp::now();
|
||||||
#[cfg(test)]
|
|
||||||
let sent_at = self
|
|
||||||
.test_hooks
|
|
||||||
.close_sent_at
|
|
||||||
.take()
|
|
||||||
.unwrap_or_else(ClientTimestamp::now);
|
|
||||||
let event = self.runtime.commit_local_close(prepared.commit, sent_at);
|
|
||||||
self.schedule = None;
|
|
||||||
self.applied_traffic_class = None;
|
|
||||||
|
|
||||||
|
let event = self.runtime.commit_local_close(prepared.commit, sent_at);
|
||||||
|
|
||||||
|
self.applied_traffic_class = None;
|
||||||
validate_datagram_length(expected_bytes, bytes)?;
|
validate_datagram_length(expected_bytes, bytes)?;
|
||||||
events.push(event);
|
events.push(event);
|
||||||
Ok(events)
|
Ok(events)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the monotonic deadline for the next probe send, if another probe
|
|
||||||
/// is scheduled.
|
|
||||||
pub fn next_send_deadline(&self) -> Option<Instant> {
|
|
||||||
if !self.runtime.is_open() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
self.schedule.as_ref()?.next_send_deadline()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the local timeout used to classify pending probes as lost.
|
/// Return the local timeout used to classify pending probes as lost.
|
||||||
pub fn probe_timeout(&self) -> Duration {
|
pub fn probe_timeout(&self) -> Duration {
|
||||||
self.runtime.probe_timeout()
|
self.runtime.probe_timeout()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send one echo probe if the negotiated run is still active.
|
/// Send one echo probe now. The caller owns pacing and run duration.
|
||||||
///
|
///
|
||||||
/// Returns an `EchoSent` event when a probe is sent. Returns an empty event
|
/// The negotiated interval and duration do not gate low-level sends.
|
||||||
/// list when the run duration has elapsed and no further probe should be
|
|
||||||
/// sent.
|
|
||||||
pub fn send_probe(&mut self) -> Result<Vec<ClientEvent>, ClientError> {
|
pub fn send_probe(&mut self) -> Result<Vec<ClientEvent>, ClientError> {
|
||||||
self.send_probe_transaction(ProbeScheduleMode::CallerPaced)
|
let remote = self.remote;
|
||||||
|
let (runtime, socket) = (&mut self.runtime, &self.socket);
|
||||||
|
let prepared = runtime.prepare_probe()?;
|
||||||
|
let machine_preflight = runtime.preflight_probe_commit(&prepared)?;
|
||||||
|
let mut events = Vec::new();
|
||||||
|
events
|
||||||
|
.try_reserve(1)
|
||||||
|
.map_err(|source| ClientError::AllocationFailed {
|
||||||
|
operation: "probe event result",
|
||||||
|
source,
|
||||||
|
})?;
|
||||||
|
let expected_bytes = prepared.bytes.len();
|
||||||
|
|
||||||
|
// All fallible probe-commit preflight (timeout deadline arithmetic,
|
||||||
|
// capacity/collision checks already done above) is finalized from
|
||||||
|
// this PRIVATE pre-send anchor, before the socket send. It is not
|
||||||
|
// the public measurement `sent_at` captured further below.
|
||||||
|
let send_anchor = ClientTimestamp::now();
|
||||||
|
let machine_commit = runtime.finalize_probe_commit(machine_preflight, send_anchor)?;
|
||||||
|
let send_call_start = Instant::now();
|
||||||
|
|
||||||
|
let bytes = match socket.send(&prepared.bytes) {
|
||||||
|
Ok(bytes) => bytes,
|
||||||
|
Err(error) => {
|
||||||
|
runtime.invalidate_kernel_tx_correlation();
|
||||||
|
return Err(ClientError::Socket(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let send_finished_at = Instant::now();
|
||||||
|
|
||||||
|
// The public measurement timestamp: paired wall/monotonic sample
|
||||||
|
// captured immediately after the successful socket send completed.
|
||||||
|
// This, not `send_anchor` above, is the RTT/OWD-fallback/timer_error
|
||||||
|
// endpoint. The infallible machine commit below never fails.
|
||||||
|
let sent_at = ClientTimestamp::now();
|
||||||
|
let sent = runtime.commit_probe_sent(machine_commit, sent_at, bytes);
|
||||||
|
let send_call = send_finished_at.saturating_duration_since(send_call_start);
|
||||||
|
validate_datagram_length(expected_bytes, bytes)?;
|
||||||
|
self.drain_tx_timestamps()?;
|
||||||
|
|
||||||
|
events.push(echo_sent_event(remote, sent, send_call));
|
||||||
|
Ok(events)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Receive and classify at most one datagram from the socket.
|
/// Receive and classify at most one datagram from the socket.
|
||||||
@@ -294,37 +228,6 @@ impl Client {
|
|||||||
self.recv_once_inner()
|
self.recv_once_inner()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Receive one datagram into `self.recv_buffer`. In test builds, a
|
|
||||||
/// pending `inject_recv_interrupted` count reports
|
|
||||||
/// `io::ErrorKind::Interrupted` instead of performing a real receive,
|
|
||||||
/// deterministically exercising interrupted-syscall retry behavior.
|
|
||||||
fn recv_datagram_once(&mut self) -> Result<ReceivedDatagram, io::Error> {
|
|
||||||
#[cfg(test)]
|
|
||||||
{
|
|
||||||
let remaining = self.test_hooks.inject_recv_interrupted.get();
|
|
||||||
if remaining > 0 {
|
|
||||||
self.test_hooks.inject_recv_interrupted.set(remaining - 1);
|
|
||||||
return Err(io::Error::from(io::ErrorKind::Interrupted));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
recv_datagram(&self.socket, &mut self.recv_buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Same as [`Self::recv_datagram_once`], receiving into a caller-supplied
|
|
||||||
/// buffer instead of `self.recv_buffer` (used before the negotiated
|
|
||||||
/// receive buffer size is known, i.e. during Open).
|
|
||||||
fn recv_datagram_once_into(&self, buf: &mut [u8]) -> Result<ReceivedDatagram, io::Error> {
|
|
||||||
#[cfg(test)]
|
|
||||||
{
|
|
||||||
let remaining = self.test_hooks.inject_recv_interrupted.get();
|
|
||||||
if remaining > 0 {
|
|
||||||
self.test_hooks.inject_recv_interrupted.set(remaining - 1);
|
|
||||||
return Err(io::Error::from(io::ErrorKind::Interrupted));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
recv_datagram(&self.socket, buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn recv_once_inner(&mut self) -> Result<Vec<ClientEvent>, ClientError> {
|
fn recv_once_inner(&mut self) -> Result<Vec<ClientEvent>, ClientError> {
|
||||||
let Some(datagram) = self.recv_datagram_retrying_interrupted()? else {
|
let Some(datagram) = self.recv_datagram_retrying_interrupted()? else {
|
||||||
return Ok(vec![]);
|
return Ok(vec![]);
|
||||||
@@ -340,12 +243,9 @@ impl Client {
|
|||||||
datagram.received_at,
|
datagram.received_at,
|
||||||
datagram.meta,
|
datagram.meta,
|
||||||
)?;
|
)?;
|
||||||
if self.runtime.is_peer_closed() {
|
if self.runtime.is_peer_closed() && self.clear_close_dscp().is_ok() {
|
||||||
self.schedule = None;
|
|
||||||
if self.clear_close_dscp().is_ok() {
|
|
||||||
self.applied_traffic_class = None;
|
self.applied_traffic_class = None;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Ok(events)
|
Ok(events)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,7 +277,7 @@ impl Client {
|
|||||||
let mut timeout_narrowed = false;
|
let mut timeout_narrowed = false;
|
||||||
|
|
||||||
let result = loop {
|
let result = loop {
|
||||||
match self.recv_datagram_once() {
|
match recv_datagram(&self.socket, &mut self.recv_buffer) {
|
||||||
Ok(datagram) => break Ok(Some(datagram)),
|
Ok(datagram) => break Ok(Some(datagram)),
|
||||||
Err(err)
|
Err(err)
|
||||||
if matches!(
|
if matches!(
|
||||||
@@ -450,17 +350,19 @@ impl Client {
|
|||||||
self.runtime.poll_timeouts_at(now)
|
self.runtime.poll_timeouts_at(now)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return whether the current run has completed.
|
/// Negotiated parameters of the currently open session.
|
||||||
///
|
pub fn negotiated_params(&self) -> Option<&crate::NegotiatedParams> {
|
||||||
/// A normal run is complete once no more probes will be sent and all
|
self.runtime.negotiated_params()
|
||||||
/// pending probes have either replied or timed out. No-test and closed
|
}
|
||||||
/// sessions are also considered complete.
|
|
||||||
pub fn is_run_complete(&self) -> bool {
|
/// Earliest timeout deadline among probes still awaiting a reply.
|
||||||
self.runtime.is_terminal()
|
pub fn next_probe_timeout_deadline(&self) -> Option<Instant> {
|
||||||
|| self
|
self.runtime.next_probe_timeout_deadline()
|
||||||
.schedule
|
}
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|schedule| schedule.is_finished() && self.runtime.pending_is_empty())
|
/// Whether any sent probes still await a reply or timeout.
|
||||||
|
pub fn has_pending_probes(&self) -> bool {
|
||||||
|
!self.runtime.pending_is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return whether the session was closed by a peer close-flagged reply.
|
/// Return whether the session was closed by a peer close-flagged reply.
|
||||||
@@ -473,130 +375,6 @@ impl Client {
|
|||||||
self.runtime.is_peer_closed()
|
self.runtime.is_peer_closed()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn send_managed_probe(
|
|
||||||
&mut self,
|
|
||||||
scheduled_at: Instant,
|
|
||||||
) -> Result<Vec<ClientEvent>, ClientError> {
|
|
||||||
self.send_probe_transaction(ProbeScheduleMode::Managed { scheduled_at })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_probe_transaction(
|
|
||||||
&mut self,
|
|
||||||
mode: ProbeScheduleMode,
|
|
||||||
) -> Result<Vec<ClientEvent>, ClientError> {
|
|
||||||
self.runtime.ensure_open()?;
|
|
||||||
#[cfg(test)]
|
|
||||||
let test_timestamps = self.probe_send_timestamps.take();
|
|
||||||
#[cfg(test)]
|
|
||||||
let fail_send = std::mem::take(&mut self.probe_send_error);
|
|
||||||
let remote = self.remote;
|
|
||||||
let (runtime, schedule, socket) = (&mut self.runtime, &mut self.schedule, &self.socket);
|
|
||||||
let schedule = schedule
|
|
||||||
.as_mut()
|
|
||||||
.expect("open sessions always have a probe schedule");
|
|
||||||
#[cfg(not(test))]
|
|
||||||
let permission_at = Instant::now();
|
|
||||||
#[cfg(test)]
|
|
||||||
let permission_at = test_timestamps
|
|
||||||
.map(|timestamps| timestamps.permission_at)
|
|
||||||
.unwrap_or_else(Instant::now);
|
|
||||||
if !schedule.permit_probe_at(permission_at) {
|
|
||||||
return Ok(Vec::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(prepared) = runtime.prepare_probe()? else {
|
|
||||||
return Ok(Vec::new());
|
|
||||||
};
|
|
||||||
let machine_preflight = runtime.preflight_probe_commit(&prepared)?;
|
|
||||||
let schedule_commit = match mode {
|
|
||||||
ProbeScheduleMode::CallerPaced => {
|
|
||||||
schedule.preflight_caller_commit(machine_preflight.next_packets_sent)?
|
|
||||||
}
|
|
||||||
#[cfg(test)]
|
|
||||||
ProbeScheduleMode::Managed { scheduled_at } => {
|
|
||||||
schedule.preflight_managed_commit(scheduled_at, permission_at)?
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let mut events = Vec::new();
|
|
||||||
events
|
|
||||||
.try_reserve(1)
|
|
||||||
.map_err(|source| ClientError::AllocationFailed {
|
|
||||||
operation: "probe event result",
|
|
||||||
source,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let expected_bytes = prepared.bytes.len();
|
|
||||||
let scheduled_at = schedule_commit.scheduled_at;
|
|
||||||
#[cfg(test)]
|
|
||||||
let reported_bytes = self.probe_reported_len.take();
|
|
||||||
// All fallible probe-commit preflight (timeout deadline arithmetic,
|
|
||||||
// capacity/collision checks already done above) is finalized from
|
|
||||||
// this PRIVATE pre-send anchor, before the socket send. It is not
|
|
||||||
// the public measurement `sent_at` captured further below.
|
|
||||||
#[cfg(not(test))]
|
|
||||||
let send_anchor = ClientTimestamp::now();
|
|
||||||
#[cfg(test)]
|
|
||||||
let send_anchor = test_timestamps
|
|
||||||
.map(|timestamps| timestamps.send_anchor)
|
|
||||||
.unwrap_or_else(ClientTimestamp::now);
|
|
||||||
let machine_commit = runtime.finalize_probe_commit(machine_preflight, send_anchor)?;
|
|
||||||
#[cfg(not(test))]
|
|
||||||
let send_call_start = Instant::now();
|
|
||||||
#[cfg(test)]
|
|
||||||
let send_call_start = test_timestamps
|
|
||||||
.map(|timestamps| timestamps.send_call_start)
|
|
||||||
.unwrap_or_else(Instant::now);
|
|
||||||
#[cfg(test)]
|
|
||||||
if fail_send {
|
|
||||||
runtime.invalidate_kernel_tx_correlation();
|
|
||||||
return Err(ClientError::Socket(io::Error::other(
|
|
||||||
"injected probe send failure",
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
let bytes = match socket.send(&prepared.bytes) {
|
|
||||||
Ok(bytes) => bytes,
|
|
||||||
Err(error) => {
|
|
||||||
runtime.invalidate_kernel_tx_correlation();
|
|
||||||
return Err(ClientError::Socket(error));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
#[cfg(not(test))]
|
|
||||||
let send_finished_at = Instant::now();
|
|
||||||
#[cfg(test)]
|
|
||||||
let send_finished_at = test_timestamps
|
|
||||||
.map(|timestamps| timestamps.send_finished_at)
|
|
||||||
.unwrap_or_else(Instant::now);
|
|
||||||
// The public measurement timestamp: paired wall/monotonic sample
|
|
||||||
// captured immediately after the successful socket send completed.
|
|
||||||
// This, not `send_anchor` above, is the RTT/OWD-fallback/timer_error
|
|
||||||
// endpoint. The infallible machine/schedule commit below never fails.
|
|
||||||
#[cfg(not(test))]
|
|
||||||
let sent_at = ClientTimestamp::now();
|
|
||||||
#[cfg(test)]
|
|
||||||
let sent_at = test_timestamps
|
|
||||||
.map(|timestamps| timestamps.sent_at)
|
|
||||||
.unwrap_or_else(ClientTimestamp::now);
|
|
||||||
let sent = runtime.commit_probe_sent(machine_commit, sent_at, bytes);
|
|
||||||
schedule.commit(schedule_commit);
|
|
||||||
|
|
||||||
let send_call = send_finished_at.saturating_duration_since(send_call_start);
|
|
||||||
let timer_error = instant_abs_diff(sent_at.mono, scheduled_at);
|
|
||||||
#[cfg(test)]
|
|
||||||
let bytes = reported_bytes.unwrap_or(bytes);
|
|
||||||
validate_datagram_length(expected_bytes, bytes)?;
|
|
||||||
self.drain_tx_timestamps()?;
|
|
||||||
|
|
||||||
events.push(echo_sent_event(
|
|
||||||
remote,
|
|
||||||
sent,
|
|
||||||
send_call,
|
|
||||||
scheduled_at,
|
|
||||||
timer_error,
|
|
||||||
));
|
|
||||||
Ok(events)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_transaction(&mut self) -> Result<OpenOutcome, ClientError> {
|
fn open_transaction(&mut self) -> Result<OpenOutcome, ClientError> {
|
||||||
let request = self.runtime.prepare_open_request()?;
|
let request = self.runtime.prepare_open_request()?;
|
||||||
let mut buf = [0_u8; MAX_OPEN_PACKET_SIZE];
|
let mut buf = [0_u8; MAX_OPEN_PACKET_SIZE];
|
||||||
@@ -620,7 +398,7 @@ impl Client {
|
|||||||
}
|
}
|
||||||
self.socket.set_read_timeout(Some(remaining))?;
|
self.socket.set_read_timeout(Some(remaining))?;
|
||||||
|
|
||||||
let datagram = match self.recv_datagram_once_into(&mut buf) {
|
let datagram = match recv_datagram(&self.socket, &mut buf) {
|
||||||
Ok(datagram) => datagram,
|
Ok(datagram) => datagram,
|
||||||
// WouldBlock/TimedOut mean the attempt's own deadline
|
// WouldBlock/TimedOut mean the attempt's own deadline
|
||||||
// elapsed; Interrupted (EINTR) means the syscall was
|
// elapsed; Interrupted (EINTR) means the syscall was
|
||||||
@@ -663,7 +441,7 @@ impl Client {
|
|||||||
return Err(failure.primary);
|
return Err(failure.primary);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let prepared = match self.prepare_client_open(machine, datagram.received_at) {
|
let prepared = match self.prepare_client_open(machine) {
|
||||||
Ok(prepared) => prepared,
|
Ok(prepared) => prepared,
|
||||||
Err(failure) => {
|
Err(failure) => {
|
||||||
self.send_cleanup_close_best_effort(failure.machine.cleanup_close_packet());
|
self.send_cleanup_close_best_effort(failure.machine.cleanup_close_packet());
|
||||||
@@ -680,32 +458,21 @@ impl Client {
|
|||||||
fn prepare_client_open(
|
fn prepare_client_open(
|
||||||
&self,
|
&self,
|
||||||
machine: PreparedOpenAcceptance,
|
machine: PreparedOpenAcceptance,
|
||||||
opened_at: ClientTimestamp,
|
|
||||||
) -> Result<PreparedClientOpen, Box<PreparedClientOpenFailure>> {
|
) -> Result<PreparedClientOpen, Box<PreparedClientOpenFailure>> {
|
||||||
let Some(negotiated) = machine.normal_negotiated() else {
|
let Some(negotiated) = machine.normal_negotiated() else {
|
||||||
return Ok(PreparedClientOpen {
|
return Ok(PreparedClientOpen {
|
||||||
machine,
|
machine,
|
||||||
schedule: None,
|
|
||||||
recv_buffer_len: None,
|
recv_buffer_len: None,
|
||||||
negotiated_traffic_class: None,
|
negotiated_traffic_class: None,
|
||||||
previous_traffic_class: self.applied_traffic_class,
|
previous_traffic_class: self.applied_traffic_class,
|
||||||
post_open_recv_timeout: self.runtime.config().socket_config.recv_timeout,
|
post_open_recv_timeout: self.runtime.config().socket_config.recv_timeout,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
let schedule = match ProbeSchedule::new(opened_at.mono, negotiated) {
|
|
||||||
Ok(schedule) => schedule,
|
|
||||||
Err(primary) => return Err(Box::new(PreparedClientOpenFailure { primary, machine })),
|
|
||||||
};
|
|
||||||
let recv_buffer_len = match recv_buffer_size(self.runtime.has_hmac(), Some(negotiated)) {
|
let recv_buffer_len = match recv_buffer_size(self.runtime.has_hmac(), Some(negotiated)) {
|
||||||
Ok(size) => size,
|
Ok(size) => size,
|
||||||
Err(primary) => return Err(Box::new(PreparedClientOpenFailure { primary, machine })),
|
Err(primary) => return Err(Box::new(PreparedClientOpenFailure { primary, machine })),
|
||||||
};
|
};
|
||||||
#[cfg(test)]
|
|
||||||
let recv_buffer_len = self
|
|
||||||
.test_hooks
|
|
||||||
.recv_buffer_len_override
|
|
||||||
.get()
|
|
||||||
.unwrap_or(recv_buffer_len);
|
|
||||||
let negotiated_traffic_class = match u8::try_from(negotiated.params.dscp) {
|
let negotiated_traffic_class = match u8::try_from(negotiated.params.dscp) {
|
||||||
Ok(traffic_class) => traffic_class,
|
Ok(traffic_class) => traffic_class,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
@@ -720,7 +487,6 @@ impl Client {
|
|||||||
|
|
||||||
Ok(PreparedClientOpen {
|
Ok(PreparedClientOpen {
|
||||||
machine,
|
machine,
|
||||||
schedule: Some(schedule),
|
|
||||||
recv_buffer_len: Some(recv_buffer_len),
|
recv_buffer_len: Some(recv_buffer_len),
|
||||||
negotiated_traffic_class: Some(negotiated_traffic_class),
|
negotiated_traffic_class: Some(negotiated_traffic_class),
|
||||||
previous_traffic_class: self.applied_traffic_class,
|
previous_traffic_class: self.applied_traffic_class,
|
||||||
@@ -734,7 +500,6 @@ impl Client {
|
|||||||
) -> Result<OpenOutcome, ClientError> {
|
) -> Result<OpenOutcome, ClientError> {
|
||||||
let PreparedClientOpen {
|
let PreparedClientOpen {
|
||||||
machine,
|
machine,
|
||||||
schedule,
|
|
||||||
recv_buffer_len,
|
recv_buffer_len,
|
||||||
negotiated_traffic_class,
|
negotiated_traffic_class,
|
||||||
previous_traffic_class,
|
previous_traffic_class,
|
||||||
@@ -755,17 +520,15 @@ impl Client {
|
|||||||
}
|
}
|
||||||
self.recv_buffer.resize(recv_buffer_len, 0);
|
self.recv_buffer.resize(recv_buffer_len, 0);
|
||||||
|
|
||||||
#[cfg(test)]
|
if let Err(primary) =
|
||||||
self.test_hooks
|
apply_traffic_class_to_socket(&self.socket, self.remote, negotiated_traffic_class)
|
||||||
.prepared_active_session_before_dscp
|
{
|
||||||
.set(machine.has_prepared_active_session());
|
|
||||||
if let Err(primary) = self.apply_open_dscp(negotiated_traffic_class) {
|
|
||||||
self.recv_buffer.truncate(previous_len);
|
self.recv_buffer.truncate(previous_len);
|
||||||
self.restore_dscp_best_effort(previous_traffic_class);
|
self.restore_dscp_best_effort(previous_traffic_class);
|
||||||
self.send_cleanup_close_best_effort(machine.cleanup_close_packet());
|
self.send_cleanup_close_best_effort(machine.cleanup_close_packet());
|
||||||
return Err(primary);
|
return Err(primary);
|
||||||
}
|
}
|
||||||
if let Err(source) = self.restore_open_read_timeout(post_open_recv_timeout) {
|
if let Err(source) = self.socket.set_read_timeout(post_open_recv_timeout) {
|
||||||
let primary = ClientError::ReadTimeoutRestore { source };
|
let primary = ClientError::ReadTimeoutRestore { source };
|
||||||
self.recv_buffer.truncate(previous_len);
|
self.recv_buffer.truncate(previous_len);
|
||||||
self.restore_dscp_best_effort(previous_traffic_class);
|
self.restore_dscp_best_effort(previous_traffic_class);
|
||||||
@@ -774,17 +537,16 @@ impl Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let outcome = self.runtime.commit_open(machine);
|
let outcome = self.runtime.commit_open(machine);
|
||||||
self.schedule = schedule;
|
|
||||||
self.applied_traffic_class = Some(negotiated_traffic_class);
|
self.applied_traffic_class = Some(negotiated_traffic_class);
|
||||||
self.tx_timestamping_enabled = try_enable_tx_timestamping(&self.socket);
|
self.tx_timestamping_enabled = try_enable_tx_timestamping(&self.socket);
|
||||||
Ok(outcome)
|
Ok(outcome)
|
||||||
} else {
|
} else {
|
||||||
debug_assert!(schedule.is_none());
|
if let Err(source) = self.socket.set_read_timeout(post_open_recv_timeout) {
|
||||||
if let Err(source) = self.restore_open_read_timeout(post_open_recv_timeout) {
|
|
||||||
return Err(ClientError::ReadTimeoutRestore { source });
|
return Err(ClientError::ReadTimeoutRestore { source });
|
||||||
}
|
}
|
||||||
let outcome = self.runtime.commit_open(machine);
|
let outcome = self.runtime.commit_open(machine);
|
||||||
self.schedule = None;
|
|
||||||
self.applied_traffic_class = None;
|
self.applied_traffic_class = None;
|
||||||
self.tx_timestamping_enabled = try_enable_tx_timestamping(&self.socket);
|
self.tx_timestamping_enabled = try_enable_tx_timestamping(&self.socket);
|
||||||
Ok(outcome)
|
Ok(outcome)
|
||||||
@@ -809,10 +571,6 @@ impl Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn restore_dscp_best_effort(&self, previous_traffic_class: Option<u8>) {
|
fn restore_dscp_best_effort(&self, previous_traffic_class: Option<u8>) {
|
||||||
#[cfg(test)]
|
|
||||||
if self.test_hooks.fail_dscp_restore.replace(false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let _ = match previous_traffic_class {
|
let _ = match previous_traffic_class {
|
||||||
Some(traffic_class) => {
|
Some(traffic_class) => {
|
||||||
apply_traffic_class_to_socket(&self.socket, self.remote, traffic_class)
|
apply_traffic_class_to_socket(&self.socket, self.remote, traffic_class)
|
||||||
@@ -822,85 +580,29 @@ impl Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn clear_close_dscp(&self) -> Result<(), ClientError> {
|
fn clear_close_dscp(&self) -> Result<(), ClientError> {
|
||||||
#[cfg(test)]
|
|
||||||
if self.test_hooks.fail_close_dscp_clear.replace(false) {
|
|
||||||
return Err(ClientError::SocketOption {
|
|
||||||
operation: "clear negotiated DSCP",
|
|
||||||
remote: self.remote,
|
|
||||||
source: io::Error::other("injected negotiated DSCP clear failure"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
clear_dscp_on_socket(&self.socket, self.remote)
|
clear_dscp_on_socket(&self.socket, self.remote)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_cleanup_close_best_effort(&self, packet: Option<&[u8]>) {
|
fn send_cleanup_close_best_effort(&self, packet: Option<&[u8]>) {
|
||||||
if let Some(packet) = packet {
|
if let Some(packet) = packet {
|
||||||
#[cfg(test)]
|
|
||||||
if self.test_hooks.fail_cleanup_send.replace(false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let _ = self.socket.send(packet);
|
let _ = self.socket.send(packet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_open_dscp(&self, traffic_class: u8) -> Result<(), ClientError> {
|
|
||||||
#[cfg(test)]
|
|
||||||
if self.test_hooks.fail_open_dscp.replace(false) {
|
|
||||||
return Err(ClientError::SocketOption {
|
|
||||||
operation: "set negotiated DSCP",
|
|
||||||
remote: self.remote,
|
|
||||||
source: io::Error::other("injected negotiated DSCP failure"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
apply_traffic_class_to_socket(&self.socket, self.remote, traffic_class)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn restore_open_read_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
|
|
||||||
#[cfg(test)]
|
|
||||||
self.test_hooks.last_restored_read_timeout.set(timeout);
|
|
||||||
#[cfg(test)]
|
|
||||||
if self.test_hooks.fail_open_timeout_restore.replace(false) {
|
|
||||||
return Err(io::Error::other(
|
|
||||||
"injected configured read timeout restoration failure",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
self.socket.set_read_timeout(timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_close_datagram(&self, packet: &[u8]) -> io::Result<usize> {
|
|
||||||
#[cfg(test)]
|
|
||||||
{
|
|
||||||
self.test_hooks
|
|
||||||
.close_send_attempts
|
|
||||||
.set(self.test_hooks.close_send_attempts.get() + 1);
|
|
||||||
if self.test_hooks.fail_close_send.replace(false) {
|
|
||||||
return Err(io::Error::other("injected close send failure"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let bytes = self.socket.send(packet)?;
|
|
||||||
#[cfg(test)]
|
|
||||||
if let Some(reported) = self.test_hooks.close_reported_len.take() {
|
|
||||||
return Ok(reported);
|
|
||||||
}
|
|
||||||
Ok(bytes)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn echo_sent_event(
|
pub(crate) fn echo_sent_event(
|
||||||
remote: SocketAddr,
|
remote: SocketAddr,
|
||||||
sent: ProbeSent,
|
sent: ProbeSent,
|
||||||
send_call: Duration,
|
send_call: Duration,
|
||||||
scheduled_at: Instant,
|
|
||||||
timer_error: Duration,
|
|
||||||
) -> ClientEvent {
|
) -> ClientEvent {
|
||||||
ClientEvent::EchoSent {
|
ClientEvent::EchoSent {
|
||||||
seq: sent.seq,
|
seq: sent.seq,
|
||||||
remote,
|
remote,
|
||||||
scheduled_at,
|
scheduled_at: None,
|
||||||
sent_at: sent.sent_at,
|
sent_at: sent.sent_at,
|
||||||
bytes: sent.bytes,
|
bytes: sent.bytes,
|
||||||
send_call,
|
send_call,
|
||||||
timer_error,
|
timer_error: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -912,46 +614,5 @@ pub(crate) fn validate_datagram_length(expected: usize, actual: usize) -> Result
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
impl Client {
|
|
||||||
fn send_probe_at(
|
|
||||||
&mut self,
|
|
||||||
timestamps: ProbeSendTimestamps,
|
|
||||||
) -> Result<Vec<ClientEvent>, ClientError> {
|
|
||||||
self.probe_send_timestamps = Some(timestamps);
|
|
||||||
self.send_probe()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_managed_probe_at(
|
|
||||||
&mut self,
|
|
||||||
scheduled_at: Instant,
|
|
||||||
timestamps: ProbeSendTimestamps,
|
|
||||||
) -> Result<Vec<ClientEvent>, ClientError> {
|
|
||||||
self.probe_send_timestamps = Some(timestamps);
|
|
||||||
self.send_managed_probe(scheduled_at)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
fn bounded_receive_timeout(
|
|
||||||
deadline: Option<Instant>,
|
|
||||||
configured_timeout: Option<Duration>,
|
|
||||||
max_wait: Duration,
|
|
||||||
now: Instant,
|
|
||||||
) -> Option<Duration> {
|
|
||||||
if max_wait.is_zero() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let mut timeout = configured_timeout.unwrap_or(max_wait).min(max_wait);
|
|
||||||
if let Some(deadline) = deadline {
|
|
||||||
let remaining = deadline.checked_duration_since(now)?;
|
|
||||||
if remaining.is_zero() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
timeout = timeout.min(remaining);
|
|
||||||
}
|
|
||||||
(!timeout.is_zero()).then_some(timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
@@ -17,191 +17,6 @@ fn open_fails_after_close() {
|
|||||||
server.join();
|
server.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn close_sends_one_close_packet_with_negotiated_token() {
|
|
||||||
let config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
|
||||||
let params = params_from_config(&config).unwrap();
|
|
||||||
let server = start_fake_server(move |socket, tx| {
|
|
||||||
let (_, peer) = recv_request(&socket, &tx);
|
|
||||||
let reply = open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None);
|
|
||||||
socket.send_to(&reply, peer).unwrap();
|
|
||||||
let _ = recv_request(&socket, &tx);
|
|
||||||
});
|
|
||||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
|
||||||
assert_open_started(client.open().unwrap());
|
|
||||||
let sent_at = ClientTimestamp {
|
|
||||||
wall: SystemTime::UNIX_EPOCH + Duration::from_secs(1_234),
|
|
||||||
mono: Instant::now() + Duration::from_secs(5),
|
|
||||||
};
|
|
||||||
client.test_hooks.close_sent_at.set(Some(sent_at));
|
|
||||||
let events = client.close().unwrap();
|
|
||||||
assert!(matches!(
|
|
||||||
events.as_slice(),
|
|
||||||
[ClientEvent::SessionClosed {
|
|
||||||
token: TOKEN,
|
|
||||||
at,
|
|
||||||
..
|
|
||||||
}] if *at == sent_at
|
|
||||||
));
|
|
||||||
assert!(!client.runtime.is_open());
|
|
||||||
assert!(client.schedule.is_none());
|
|
||||||
assert_eq!(client.applied_traffic_class, None);
|
|
||||||
let packets: Vec<_> = server.rx.iter().take(2).collect();
|
|
||||||
let close = &packets[1];
|
|
||||||
assert_eq!(close[3], flags::FLAG_CLOSE);
|
|
||||||
assert_eq!(u64::from_le_bytes(close[4..12].try_into().unwrap()), TOKEN);
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn close_event_reservation_failure_precedes_dscp_clear_and_send() {
|
|
||||||
let params = default_params();
|
|
||||||
let server = start_fake_server(move |socket, tx| {
|
|
||||||
let (_, peer) = recv_request(&socket, &tx);
|
|
||||||
socket
|
|
||||||
.send_to(
|
|
||||||
&open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None),
|
|
||||||
peer,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let _ = recv_request(&socket, &tx);
|
|
||||||
});
|
|
||||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
|
||||||
assert_open_started(client.open().unwrap());
|
|
||||||
client.test_hooks.fail_close_event_reserve.set(true);
|
|
||||||
client.test_hooks.fail_close_dscp_clear.set(true);
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
client.close(),
|
|
||||||
Err(ClientError::AllocationFailed {
|
|
||||||
operation: "close event result",
|
|
||||||
..
|
|
||||||
})
|
|
||||||
));
|
|
||||||
assert_eq!(client.test_hooks.close_send_attempts.get(), 0);
|
|
||||||
assert!(client.test_hooks.fail_close_dscp_clear.get());
|
|
||||||
assert!(client.runtime.is_open());
|
|
||||||
assert!(client.schedule.is_some());
|
|
||||||
assert_eq!(client.applied_traffic_class, Some(0));
|
|
||||||
|
|
||||||
client.test_hooks.fail_close_dscp_clear.set(false);
|
|
||||||
client.close().unwrap();
|
|
||||||
assert_eq!(client.test_hooks.close_send_attempts.get(), 1);
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn close_dscp_clear_failure_precedes_send_and_preserves_open_state() {
|
|
||||||
let params = default_params();
|
|
||||||
let server = start_fake_server(move |socket, tx| {
|
|
||||||
let (_, peer) = recv_request(&socket, &tx);
|
|
||||||
socket
|
|
||||||
.send_to(
|
|
||||||
&open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None),
|
|
||||||
peer,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let _ = recv_request(&socket, &tx);
|
|
||||||
});
|
|
||||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
|
||||||
assert_open_started(client.open().unwrap());
|
|
||||||
client.test_hooks.fail_close_dscp_clear.set(true);
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
client.close(),
|
|
||||||
Err(ClientError::SocketOption { .. })
|
|
||||||
));
|
|
||||||
assert_eq!(client.test_hooks.close_send_attempts.get(), 0);
|
|
||||||
assert!(client.runtime.is_open());
|
|
||||||
assert!(client.schedule.is_some());
|
|
||||||
assert_eq!(client.applied_traffic_class, Some(0));
|
|
||||||
|
|
||||||
client.close().unwrap();
|
|
||||||
assert_eq!(client.test_hooks.close_send_attempts.get(), 1);
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn close_send_failure_leaves_machine_and_schedule_open_for_retry() {
|
|
||||||
let params = default_params();
|
|
||||||
let server = start_fake_server(move |socket, tx| {
|
|
||||||
let (_, peer) = recv_request(&socket, &tx);
|
|
||||||
socket
|
|
||||||
.send_to(
|
|
||||||
&open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None),
|
|
||||||
peer,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let _ = recv_request(&socket, &tx);
|
|
||||||
});
|
|
||||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
|
||||||
assert_open_started(client.open().unwrap());
|
|
||||||
let sent_at = ClientTimestamp {
|
|
||||||
wall: SystemTime::UNIX_EPOCH + Duration::from_secs(2_345),
|
|
||||||
mono: Instant::now() + Duration::from_secs(6),
|
|
||||||
};
|
|
||||||
client.test_hooks.close_sent_at.set(Some(sent_at));
|
|
||||||
client.test_hooks.fail_close_send.set(true);
|
|
||||||
|
|
||||||
assert!(matches!(client.close(), Err(ClientError::Socket(_))));
|
|
||||||
assert_eq!(client.test_hooks.close_sent_at.get(), Some(sent_at));
|
|
||||||
assert!(client.runtime.is_open());
|
|
||||||
assert!(client.schedule.is_some());
|
|
||||||
assert_eq!(client.applied_traffic_class, Some(0));
|
|
||||||
assert_eq!(client.test_hooks.close_send_attempts.get(), 1);
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
client.close().unwrap().as_slice(),
|
|
||||||
[ClientEvent::SessionClosed {
|
|
||||||
token: TOKEN,
|
|
||||||
at,
|
|
||||||
..
|
|
||||||
}] if *at == sent_at
|
|
||||||
));
|
|
||||||
assert_eq!(client.test_hooks.close_sent_at.get(), None);
|
|
||||||
assert_eq!(client.test_hooks.close_send_attempts.get(), 2);
|
|
||||||
assert!(matches!(client.close(), Err(ClientError::AlreadyClosed)));
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn short_successful_close_commits_before_length_mismatch() {
|
|
||||||
let params = default_params();
|
|
||||||
let server = start_fake_server(move |socket, tx| {
|
|
||||||
let (_, peer) = recv_request(&socket, &tx);
|
|
||||||
socket
|
|
||||||
.send_to(
|
|
||||||
&open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None),
|
|
||||||
peer,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let _ = recv_request(&socket, &tx);
|
|
||||||
});
|
|
||||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
|
||||||
assert_open_started(client.open().unwrap());
|
|
||||||
let expected = client.runtime.prepare_close().unwrap().bytes.len();
|
|
||||||
let sent_at = ClientTimestamp {
|
|
||||||
wall: SystemTime::UNIX_EPOCH + Duration::from_secs(3_456),
|
|
||||||
mono: Instant::now() + Duration::from_secs(7),
|
|
||||||
};
|
|
||||||
client.test_hooks.close_sent_at.set(Some(sent_at));
|
|
||||||
client.test_hooks.close_reported_len.set(Some(expected - 1));
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
client.close(),
|
|
||||||
Err(ClientError::DatagramLengthMismatch {
|
|
||||||
expected: error_expected,
|
|
||||||
actual,
|
|
||||||
}) if error_expected == expected && actual + 1 == expected
|
|
||||||
));
|
|
||||||
assert!(!client.runtime.is_open());
|
|
||||||
assert!(client.schedule.is_none());
|
|
||||||
assert_eq!(client.applied_traffic_class, None);
|
|
||||||
assert_eq!(client.test_hooks.close_sent_at.get(), None);
|
|
||||||
assert!(matches!(client.close(), Err(ClientError::AlreadyClosed)));
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn send_probe_fails_after_close() {
|
fn send_probe_fails_after_close() {
|
||||||
let params = default_params();
|
let params = default_params();
|
||||||
@@ -232,135 +47,6 @@ fn send_probe_fails_after_close() {
|
|||||||
server.join();
|
server.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn close_flagged_echo_reply_emits_reply_then_closes_without_sending_close() {
|
|
||||||
let params = default_params();
|
|
||||||
let server = start_fake_server({
|
|
||||||
let params = params.clone();
|
|
||||||
move |socket, tx| {
|
|
||||||
let (_, peer) = recv_request(&socket, &tx);
|
|
||||||
let reply = open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None);
|
|
||||||
socket.send_to(&reply, peer).unwrap();
|
|
||||||
|
|
||||||
let (request, _) = recv_request(&socket, &tx);
|
|
||||||
let seq = u32::from_le_bytes(request[12..16].try_into().unwrap());
|
|
||||||
let reply = echo_reply_packet_with_flags(
|
|
||||||
TOKEN,
|
|
||||||
seq,
|
|
||||||
¶ms,
|
|
||||||
&TimestampFields::default(),
|
|
||||||
None,
|
|
||||||
FLAG_REPLY | flags::FLAG_CLOSE,
|
|
||||||
);
|
|
||||||
socket.send_to(&reply, peer).unwrap();
|
|
||||||
|
|
||||||
socket
|
|
||||||
.set_read_timeout(Some(Duration::from_millis(250)))
|
|
||||||
.unwrap();
|
|
||||||
while recv_request_timeout(&socket, &tx).is_some() {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let config = ClientConfig {
|
|
||||||
socket_config: crate::SocketConfig {
|
|
||||||
recv_timeout: Some(Duration::from_millis(200)),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..default_test_config(server.addr)
|
|
||||||
};
|
|
||||||
let mut client = Client::connect(config).unwrap();
|
|
||||||
assert_open_started(client.open().unwrap());
|
|
||||||
client.send_probe().unwrap();
|
|
||||||
|
|
||||||
let events = client.recv_once().unwrap();
|
|
||||||
assert!(matches!(
|
|
||||||
events.first(),
|
|
||||||
Some(ClientEvent::EchoReply { .. })
|
|
||||||
));
|
|
||||||
assert!(matches!(
|
|
||||||
events.get(1),
|
|
||||||
Some(ClientEvent::SessionClosed { token: TOKEN, .. })
|
|
||||||
));
|
|
||||||
assert_eq!(events.len(), 2);
|
|
||||||
assert!(client.next_send_deadline().is_none());
|
|
||||||
assert!(client.schedule.is_none());
|
|
||||||
assert!(matches!(
|
|
||||||
client.send_probe(),
|
|
||||||
Err(ClientError::AlreadyClosed)
|
|
||||||
));
|
|
||||||
|
|
||||||
let first = server.rx.recv_timeout(Duration::from_millis(100)).unwrap();
|
|
||||||
let second = server.rx.recv_timeout(Duration::from_millis(100)).unwrap();
|
|
||||||
assert_eq!(first[3] & FLAG_OPEN, FLAG_OPEN);
|
|
||||||
assert_eq!(second[3] & flags::FLAG_CLOSE, 0);
|
|
||||||
assert!(server.rx.recv_timeout(Duration::from_millis(400)).is_err());
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn peer_close_dscp_cleanup_failure_preserves_events_and_closed_state() {
|
|
||||||
let params = default_params();
|
|
||||||
let server = start_fake_server({
|
|
||||||
let params = params.clone();
|
|
||||||
move |socket, tx| {
|
|
||||||
let (_, peer) = recv_request(&socket, &tx);
|
|
||||||
socket
|
|
||||||
.send_to(
|
|
||||||
&open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None),
|
|
||||||
peer,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let (request, _) = recv_request(&socket, &tx);
|
|
||||||
let seq = u32::from_le_bytes(request[12..16].try_into().unwrap());
|
|
||||||
socket
|
|
||||||
.send_to(
|
|
||||||
&echo_reply_packet_with_flags(
|
|
||||||
TOKEN,
|
|
||||||
seq,
|
|
||||||
¶ms,
|
|
||||||
&TimestampFields::default(),
|
|
||||||
None,
|
|
||||||
FLAG_REPLY | flags::FLAG_CLOSE,
|
|
||||||
),
|
|
||||||
peer,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
socket
|
|
||||||
.set_read_timeout(Some(Duration::from_millis(250)))
|
|
||||||
.unwrap();
|
|
||||||
while recv_request_timeout(&socket, &tx).is_some() {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let mut client = Client::connect(ClientConfig {
|
|
||||||
socket_config: crate::SocketConfig {
|
|
||||||
recv_timeout: Some(Duration::from_millis(200)),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..default_test_config(server.addr)
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
assert_open_started(client.open().unwrap());
|
|
||||||
client.send_probe().unwrap();
|
|
||||||
client.test_hooks.fail_close_dscp_clear.set(true);
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
client.recv_once().unwrap().as_slice(),
|
|
||||||
[
|
|
||||||
ClientEvent::EchoReply { .. },
|
|
||||||
ClientEvent::SessionClosed { token: TOKEN, .. }
|
|
||||||
]
|
|
||||||
));
|
|
||||||
assert!(client.is_peer_closed());
|
|
||||||
assert!(client.schedule.is_none());
|
|
||||||
assert_eq!(client.applied_traffic_class, Some(0));
|
|
||||||
assert_eq!(client.test_hooks.close_send_attempts.get(), 0);
|
|
||||||
let open = server.rx.recv_timeout(Duration::from_millis(100)).unwrap();
|
|
||||||
let echo = server.rx.recv_timeout(Duration::from_millis(100)).unwrap();
|
|
||||||
assert_ne!(open[3] & FLAG_OPEN, 0);
|
|
||||||
assert_eq!(echo[3] & flags::FLAG_CLOSE, 0);
|
|
||||||
assert!(server.rx.recv_timeout(Duration::from_millis(400)).is_err());
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn recv_available_stops_after_peer_close() {
|
fn recv_available_stops_after_peer_close() {
|
||||||
let params = default_params();
|
let params = default_params();
|
||||||
@@ -415,7 +101,6 @@ fn normal_echo_reply_does_not_close_session() {
|
|||||||
|
|
||||||
let events = client.recv_once().unwrap();
|
let events = client.recv_once().unwrap();
|
||||||
assert!(matches!(events.as_slice(), [ClientEvent::EchoReply { .. }]));
|
assert!(matches!(events.as_slice(), [ClientEvent::EchoReply { .. }]));
|
||||||
assert!(client.next_send_deadline().is_some());
|
|
||||||
assert!(client.send_probe().is_ok());
|
assert!(client.send_probe().is_ok());
|
||||||
|
|
||||||
client.close().unwrap();
|
client.close().unwrap();
|
||||||
@@ -752,3 +437,66 @@ fn wrong_token_close_flag_does_not_close_session() {
|
|||||||
client.close().unwrap();
|
client.close().unwrap();
|
||||||
server.join();
|
server.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn close_flagged_echo_reply_emits_reply_then_closes_without_sending_close() {
|
||||||
|
let params = default_params();
|
||||||
|
let server = start_fake_server({
|
||||||
|
let params = params.clone();
|
||||||
|
move |socket, tx| {
|
||||||
|
let (_, peer) = recv_request(&socket, &tx);
|
||||||
|
let reply = open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None);
|
||||||
|
socket.send_to(&reply, peer).unwrap();
|
||||||
|
|
||||||
|
let (request, _) = recv_request(&socket, &tx);
|
||||||
|
let seq = u32::from_le_bytes(request[12..16].try_into().unwrap());
|
||||||
|
let reply = echo_reply_packet_with_flags(
|
||||||
|
TOKEN,
|
||||||
|
seq,
|
||||||
|
¶ms,
|
||||||
|
&TimestampFields::default(),
|
||||||
|
None,
|
||||||
|
FLAG_REPLY | flags::FLAG_CLOSE,
|
||||||
|
);
|
||||||
|
socket.send_to(&reply, peer).unwrap();
|
||||||
|
|
||||||
|
socket
|
||||||
|
.set_read_timeout(Some(Duration::from_millis(250)))
|
||||||
|
.unwrap();
|
||||||
|
while recv_request_timeout(&socket, &tx).is_some() {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let config = ClientConfig {
|
||||||
|
socket_config: crate::SocketConfig {
|
||||||
|
recv_timeout: Some(Duration::from_millis(200)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..default_test_config(server.addr)
|
||||||
|
};
|
||||||
|
let mut client = Client::connect(config).unwrap();
|
||||||
|
assert_open_started(client.open().unwrap());
|
||||||
|
client.send_probe().unwrap();
|
||||||
|
|
||||||
|
let events = client.recv_once().unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
events.first(),
|
||||||
|
Some(ClientEvent::EchoReply { .. })
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
events.get(1),
|
||||||
|
Some(ClientEvent::SessionClosed { token: TOKEN, .. })
|
||||||
|
));
|
||||||
|
assert_eq!(events.len(), 2);
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
client.send_probe(),
|
||||||
|
Err(ClientError::AlreadyClosed)
|
||||||
|
));
|
||||||
|
|
||||||
|
let first = server.rx.recv_timeout(Duration::from_millis(100)).unwrap();
|
||||||
|
let second = server.rx.recv_timeout(Duration::from_millis(100)).unwrap();
|
||||||
|
assert_eq!(first[3] & FLAG_OPEN, FLAG_OPEN);
|
||||||
|
assert_eq!(second[3] & flags::FLAG_CLOSE, 0);
|
||||||
|
assert!(server.rx.recv_timeout(Duration::from_millis(400)).is_err());
|
||||||
|
server.join();
|
||||||
|
}
|
||||||
@@ -82,80 +82,9 @@ fn normal_open_uses_negotiated_traffic_class_not_requested_dscp() {
|
|||||||
target_os = "redox",
|
target_os = "redox",
|
||||||
target_os = "solaris",
|
target_os = "solaris",
|
||||||
target_os = "illumos",
|
target_os = "illumos",
|
||||||
target_os = "haiku",
|
target_os = "haiku"
|
||||||
)))]
|
)))]
|
||||||
fn failed_close_send_restores_negotiated_traffic_class_and_keeps_session_open() {
|
fn authenticated_peer_close_clears_negotiated_traffic_class() {
|
||||||
let mut params = default_params();
|
|
||||||
// Raw wire TOS/Traffic Class byte for DSCP codepoint 46 (EF).
|
|
||||||
params.dscp = 184;
|
|
||||||
let server = start_fake_server(move |socket, tx| {
|
|
||||||
let (_, peer) = recv_request(&socket, &tx);
|
|
||||||
socket
|
|
||||||
.send_to(
|
|
||||||
&open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None),
|
|
||||||
peer,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let _ = recv_request(&socket, &tx);
|
|
||||||
});
|
|
||||||
let mut config = default_test_config(server.addr);
|
|
||||||
config.dscp = 46;
|
|
||||||
let mut client = Client::connect(config).unwrap();
|
|
||||||
assert_open_started(client.open().unwrap());
|
|
||||||
client.test_hooks.fail_close_send.set(true);
|
|
||||||
|
|
||||||
let error = client.close().unwrap_err();
|
|
||||||
|
|
||||||
assert!(matches!(error, ClientError::Socket(_)));
|
|
||||||
assert!(client.runtime.is_open());
|
|
||||||
assert!(client.schedule.is_some());
|
|
||||||
assert_eq!(client.applied_traffic_class, Some(184));
|
|
||||||
assert_eq!(
|
|
||||||
socket_traffic_class(&client.socket, client.remote).unwrap() & 0xfc,
|
|
||||||
184
|
|
||||||
);
|
|
||||||
client.close().unwrap();
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn failed_close_keeps_send_error_primary_when_dscp_restoration_also_fails() {
|
|
||||||
let params = default_params();
|
|
||||||
let server = start_fake_server(move |socket, tx| {
|
|
||||||
let (_, peer) = recv_request(&socket, &tx);
|
|
||||||
socket
|
|
||||||
.send_to(
|
|
||||||
&open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None),
|
|
||||||
peer,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let _ = recv_request(&socket, &tx);
|
|
||||||
});
|
|
||||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
|
||||||
assert_open_started(client.open().unwrap());
|
|
||||||
client.test_hooks.fail_close_send.set(true);
|
|
||||||
client.test_hooks.fail_dscp_restore.set(true);
|
|
||||||
|
|
||||||
let error = client.close().unwrap_err();
|
|
||||||
|
|
||||||
assert!(matches!(error, ClientError::Socket(_)));
|
|
||||||
assert!(error.to_string().contains("injected close send failure"));
|
|
||||||
assert!(client.runtime.is_open());
|
|
||||||
assert!(client.schedule.is_some());
|
|
||||||
assert_eq!(client.applied_traffic_class, Some(0));
|
|
||||||
client.close().unwrap();
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg(not(any(
|
|
||||||
target_os = "fuchsia",
|
|
||||||
target_os = "redox",
|
|
||||||
target_os = "solaris",
|
|
||||||
target_os = "illumos",
|
|
||||||
target_os = "haiku",
|
|
||||||
)))]
|
|
||||||
fn authenticated_peer_close_clears_schedule_and_negotiated_traffic_class() {
|
|
||||||
let mut params = default_params();
|
let mut params = default_params();
|
||||||
// Raw wire TOS/Traffic Class byte for DSCP codepoint 46 (EF).
|
// Raw wire TOS/Traffic Class byte for DSCP codepoint 46 (EF).
|
||||||
params.dscp = 184;
|
params.dscp = 184;
|
||||||
@@ -198,7 +127,7 @@ fn authenticated_peer_close_clears_schedule_and_negotiated_traffic_class() {
|
|||||||
ClientEvent::SessionClosed { .. }
|
ClientEvent::SessionClosed { .. }
|
||||||
]
|
]
|
||||||
));
|
));
|
||||||
assert!(client.schedule.is_none());
|
|
||||||
assert_eq!(client.applied_traffic_class, None);
|
assert_eq!(client.applied_traffic_class, None);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
socket_traffic_class(&client.socket, client.remote).unwrap(),
|
socket_traffic_class(&client.socket, client.remote).unwrap(),
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::config::{NegotiationPolicy, DEFAULT_OPEN_TIMEOUTS};
|
use crate::config::{NegotiationPolicy, DEFAULT_OPEN_TIMEOUTS};
|
||||||
|
use crate::{
|
||||||
|
session::machine::{
|
||||||
|
compute_one_way, compute_rtt, params_from_config, sequence_is_after, sequence_is_before,
|
||||||
|
unix_epoch_ns_i64, update_highest_received,
|
||||||
|
},
|
||||||
|
session::negotiate_params,
|
||||||
|
NegotiatedParams, RunMode, SignedDuration, WarningKind, MAX_UDP_PAYLOAD_LENGTH,
|
||||||
|
};
|
||||||
|
use irtt_proto::{flags, Params, TimestampFields, PROTOCOL_VERSION};
|
||||||
use irtt_proto::{
|
use irtt_proto::{
|
||||||
flags::FLAG_HMAC, flags::FLAG_OPEN, flags::FLAG_REPLY, verify_packet_hmac, Clock,
|
flags::FLAG_HMAC, flags::FLAG_OPEN, flags::FLAG_REPLY, verify_packet_hmac, Clock,
|
||||||
ReceivedStats, StampAt, HMAC_SIZE, MAGIC,
|
ReceivedStats, StampAt, HMAC_SIZE, MAGIC,
|
||||||
|
|||||||
@@ -1,22 +1,5 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn no_test_success_validates_params() {
|
|
||||||
let mut config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
|
||||||
config.run_mode = RunMode::NoTest;
|
|
||||||
let params = params_from_config(&config).unwrap();
|
|
||||||
let server = no_test_server(params.clone(), 0);
|
|
||||||
config.server_addr = server.addr.to_string();
|
|
||||||
let mut client = Client::connect(config).unwrap();
|
|
||||||
assert!(client.next_send_deadline().is_none());
|
|
||||||
let negotiated = assert_no_test_completed(client.open().unwrap());
|
|
||||||
assert_eq!(negotiated.params, params);
|
|
||||||
assert!(client.next_send_deadline().is_none());
|
|
||||||
assert!(client.schedule.is_none());
|
|
||||||
assert_eq!(client.applied_traffic_class, None);
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn no_test_rejects_non_close_open_reply() {
|
fn no_test_rejects_non_close_open_reply() {
|
||||||
let mut config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
let mut config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
||||||
@@ -48,32 +31,6 @@ fn no_test_rejects_non_close_open_reply() {
|
|||||||
server.join();
|
server.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn no_test_cleanup_send_failure_preserves_unexpected_reply() {
|
|
||||||
let mut config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
|
||||||
config.run_mode = RunMode::NoTest;
|
|
||||||
let params = params_from_config(&config).unwrap();
|
|
||||||
let server = start_fake_server(move |socket, tx| {
|
|
||||||
let (_, peer) = recv_request(&socket, &tx);
|
|
||||||
socket
|
|
||||||
.send_to(
|
|
||||||
&open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None),
|
|
||||||
peer,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
});
|
|
||||||
config.server_addr = server.addr.to_string();
|
|
||||||
let mut client = Client::connect(config).unwrap();
|
|
||||||
client.test_hooks.fail_cleanup_send.set(true);
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
client.open(),
|
|
||||||
Err(ClientError::UnexpectedNoTestReply)
|
|
||||||
));
|
|
||||||
assert!(client.runtime.prepare_open_request().is_ok());
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn no_test_rejects_non_zero_token_with_close_reply() {
|
fn no_test_rejects_non_zero_token_with_close_reply() {
|
||||||
let mut config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
let mut config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
||||||
@@ -156,3 +113,19 @@ fn open_fails_after_no_test_completed() {
|
|||||||
assert!(matches!(client.open(), Err(ClientError::AlreadyCompleted)));
|
assert!(matches!(client.open(), Err(ClientError::AlreadyCompleted)));
|
||||||
server.join();
|
server.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_test_success_validates_params() {
|
||||||
|
let mut config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
||||||
|
config.run_mode = RunMode::NoTest;
|
||||||
|
let params = params_from_config(&config).unwrap();
|
||||||
|
let server = no_test_server(params.clone(), 0);
|
||||||
|
config.server_addr = server.addr.to_string();
|
||||||
|
let mut client = Client::connect(config).unwrap();
|
||||||
|
|
||||||
|
let negotiated = assert_no_test_completed(client.open().unwrap());
|
||||||
|
assert_eq!(negotiated.params, params);
|
||||||
|
|
||||||
|
assert_eq!(client.applied_traffic_class, None);
|
||||||
|
server.join();
|
||||||
|
}
|
||||||
@@ -1,37 +1,5 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn successful_open_handshake() {
|
|
||||||
let config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
|
||||||
let params = params_from_config(&config).unwrap();
|
|
||||||
let server = open_success_server(params.clone());
|
|
||||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
|
||||||
|
|
||||||
let negotiated = assert_open_started(client.open().unwrap());
|
|
||||||
assert_eq!(negotiated.params, params);
|
|
||||||
assert!(client.schedule.is_some());
|
|
||||||
assert_eq!(client.applied_traffic_class, Some(0));
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn successful_open_restores_configured_receive_timeout() {
|
|
||||||
let config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
|
||||||
let params = params_from_config(&config).unwrap();
|
|
||||||
let server = open_success_server(params);
|
|
||||||
let mut config = default_test_config(server.addr);
|
|
||||||
config.socket_config.recv_timeout = Some(Duration::from_millis(75));
|
|
||||||
let mut client = Client::connect(config).unwrap();
|
|
||||||
|
|
||||||
assert_open_started(client.open().unwrap());
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
client.test_hooks.last_restored_read_timeout.get(),
|
|
||||||
Some(Duration::from_millis(75))
|
|
||||||
);
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn open_fails_when_already_open() {
|
fn open_fails_when_already_open() {
|
||||||
let config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
let config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
||||||
@@ -64,84 +32,6 @@ fn open_retries_after_first_timeout() {
|
|||||||
server.join();
|
server.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn open_survives_interrupted_receive_within_same_attempt() {
|
|
||||||
let server = start_fake_server(|socket, tx| {
|
|
||||||
let (first, peer) = recv_request(&socket, &tx);
|
|
||||||
let params = Params::decode(&first[4..]).unwrap();
|
|
||||||
let reply = open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None);
|
|
||||||
socket.send_to(&reply, peer).unwrap();
|
|
||||||
});
|
|
||||||
let config = ClientConfig {
|
|
||||||
open_timeouts: vec![Duration::from_millis(500)],
|
|
||||||
..default_test_config(server.addr)
|
|
||||||
};
|
|
||||||
let mut client = Client::connect(config).unwrap();
|
|
||||||
// Several interrupted receive attempts before the real reply arrives
|
|
||||||
// must not be treated as a timeout, must not retransmit the open
|
|
||||||
// request, and must not advance to a next configured attempt.
|
|
||||||
client.test_hooks.inject_recv_interrupted.set(5);
|
|
||||||
|
|
||||||
let outcome = client.open().unwrap();
|
|
||||||
assert_open_started(outcome);
|
|
||||||
// Exactly one request reached the server: interruptions were absorbed
|
|
||||||
// inside the single configured attempt.
|
|
||||||
assert_eq!(server.rx.iter().take(1).count(), 1);
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn open_interrupted_receive_does_not_extend_the_attempt_deadline() {
|
|
||||||
// A fake server that never replies: with a short attempt timeout, the
|
|
||||||
// real question is whether interruptions can push the effective wait
|
|
||||||
// past the configured deadline. They must not: the deadline stays
|
|
||||||
// absolute across any number of interruptions.
|
|
||||||
let server = start_fake_server(|socket, tx| {
|
|
||||||
let _ = recv_request(&socket, &tx);
|
|
||||||
});
|
|
||||||
let config = ClientConfig {
|
|
||||||
open_timeouts: vec![Duration::from_millis(200)],
|
|
||||||
..default_test_config(server.addr)
|
|
||||||
};
|
|
||||||
let mut client = Client::connect(config).unwrap();
|
|
||||||
// Each interruption carries no timing information of its own, so the
|
|
||||||
// loop must keep re-checking the real deadline rather than treating an
|
|
||||||
// interruption as "more time available" or a reason to fail outright.
|
|
||||||
client.test_hooks.inject_recv_interrupted.set(20);
|
|
||||||
|
|
||||||
let started = Instant::now();
|
|
||||||
let result = client.open();
|
|
||||||
let elapsed = started.elapsed();
|
|
||||||
|
|
||||||
assert!(matches!(result, Err(ClientError::OpenTimeout)));
|
|
||||||
assert!(
|
|
||||||
elapsed < Duration::from_millis(800),
|
|
||||||
"interrupted receives must not extend the attempt deadline, took {elapsed:?}"
|
|
||||||
);
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn open_timeout_after_all_timeouts() {
|
|
||||||
let server = timeout_server(Duration::from_millis(700));
|
|
||||||
let config = ClientConfig {
|
|
||||||
open_timeouts: vec![Duration::from_millis(200), Duration::from_millis(200)],
|
|
||||||
socket_config: crate::SocketConfig {
|
|
||||||
recv_timeout: Some(Duration::from_millis(50)),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..default_test_config(server.addr)
|
|
||||||
};
|
|
||||||
let mut client = Client::connect(config).unwrap();
|
|
||||||
assert!(matches!(client.open(), Err(ClientError::OpenTimeout)));
|
|
||||||
assert_eq!(server.rx.iter().take(2).count(), 2);
|
|
||||||
assert_eq!(
|
|
||||||
client.test_hooks.last_restored_read_timeout.get(),
|
|
||||||
Some(Duration::from_millis(50))
|
|
||||||
);
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn protocol_version_mismatch_fails() {
|
fn protocol_version_mismatch_fails() {
|
||||||
let mut config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
let mut config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
||||||
@@ -175,41 +65,6 @@ fn server_rejection_fails_in_normal_mode() {
|
|||||||
server.join();
|
server.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn multiple_untrusted_datagrams_before_valid_reply_use_one_attempt() {
|
|
||||||
let params = default_params();
|
|
||||||
let server = start_fake_server(move |socket, tx| {
|
|
||||||
let (_, peer) = recv_request(&socket, &tx);
|
|
||||||
let valid = open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None);
|
|
||||||
let mut wrong_type = valid.clone();
|
|
||||||
wrong_type[3] = FLAG_REPLY;
|
|
||||||
let mut reserved_flags = valid.clone();
|
|
||||||
reserved_flags[3] |= 0x10;
|
|
||||||
let mut bad_magic = valid.clone();
|
|
||||||
bad_magic[0] ^= 0xff;
|
|
||||||
let unexpected_hmac =
|
|
||||||
open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, Some(b"unexpected"));
|
|
||||||
|
|
||||||
for packet in [
|
|
||||||
vec![0_u8],
|
|
||||||
bad_magic,
|
|
||||||
wrong_type,
|
|
||||||
reserved_flags,
|
|
||||||
unexpected_hmac,
|
|
||||||
valid,
|
|
||||||
] {
|
|
||||||
socket.send_to(&packet, peer).unwrap();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
|
||||||
|
|
||||||
assert_open_started(client.open().unwrap());
|
|
||||||
assert_eq!(server.rx.iter().take(1).count(), 1);
|
|
||||||
assert!(server.rx.try_recv().is_err());
|
|
||||||
assert!(client.schedule.is_some());
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ignored_datagrams_do_not_restart_the_attempt_deadline() {
|
fn ignored_datagrams_do_not_restart_the_attempt_deadline() {
|
||||||
let server = start_fake_server(move |socket, tx| {
|
let server = start_fake_server(move |socket, tx| {
|
||||||
@@ -237,60 +92,6 @@ fn ignored_datagrams_do_not_restart_the_attempt_deadline() {
|
|||||||
server.join();
|
server.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn only_ignored_open_datagrams_eventually_time_out() {
|
|
||||||
let server = start_fake_server(move |socket, tx| {
|
|
||||||
for _ in 0..2 {
|
|
||||||
let (_, peer) = recv_request(&socket, &tx);
|
|
||||||
socket.send_to(&[0_u8], peer).unwrap();
|
|
||||||
socket.send_to(&MAGIC, peer).unwrap();
|
|
||||||
}
|
|
||||||
thread::sleep(Duration::from_millis(300));
|
|
||||||
});
|
|
||||||
let config = ClientConfig {
|
|
||||||
open_timeouts: vec![Duration::from_millis(200), Duration::from_millis(200)],
|
|
||||||
socket_config: crate::SocketConfig {
|
|
||||||
recv_timeout: Some(Duration::from_millis(50)),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..default_test_config(server.addr)
|
|
||||||
};
|
|
||||||
let mut client = Client::connect(config).unwrap();
|
|
||||||
|
|
||||||
assert!(matches!(client.open(), Err(ClientError::OpenTimeout)));
|
|
||||||
assert_eq!(server.rx.iter().take(2).count(), 2);
|
|
||||||
assert!(server.rx.try_recv().is_err());
|
|
||||||
assert_eq!(
|
|
||||||
client.test_hooks.last_restored_read_timeout.get(),
|
|
||||||
Some(Duration::from_millis(50))
|
|
||||||
);
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn trusted_zero_token_normal_reply_is_terminal() {
|
|
||||||
let params = default_params();
|
|
||||||
let server = start_fake_server(move |socket, tx| {
|
|
||||||
let (_, peer) = recv_request(&socket, &tx);
|
|
||||||
// encode_open_reply rejects a zero token without FLAG_CLOSE, so this
|
|
||||||
// deliberately non-compliant reply is built by encoding a normal
|
|
||||||
// reply with a placeholder token and then zeroing the token field.
|
|
||||||
let mut reply = open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None);
|
|
||||||
reply[HMAC_OFFSET..HMAC_OFFSET + 8].copy_from_slice(&0_u64.to_le_bytes());
|
|
||||||
socket.send_to(&reply, peer).unwrap();
|
|
||||||
});
|
|
||||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
client.open(),
|
|
||||||
Err(ClientError::Protocol(irtt_proto::ProtoError::ZeroToken))
|
|
||||||
));
|
|
||||||
assert_eq!(server.rx.iter().take(1).count(), 1);
|
|
||||||
assert!(client.runtime.prepare_open_request().is_ok());
|
|
||||||
assert!(client.schedule.is_none());
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn opening_deadline_overflow_occurs_before_send() {
|
fn opening_deadline_overflow_occurs_before_send() {
|
||||||
let server = timeout_server(Duration::from_millis(250));
|
let server = timeout_server(Duration::from_millis(250));
|
||||||
@@ -305,47 +106,6 @@ fn opening_deadline_overflow_occurs_before_send() {
|
|||||||
server.join();
|
server.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn schedule_duration_overflow_cannot_partially_open() {
|
|
||||||
let server = start_fake_server(|_socket, _tx| {});
|
|
||||||
let client = Client::connect(default_test_config(server.addr)).unwrap();
|
|
||||||
let reply = irtt_proto::OpenReply {
|
|
||||||
flags: FLAG_OPEN | FLAG_REPLY,
|
|
||||||
token: TOKEN,
|
|
||||||
params: params_from_config(client.runtime.config()).unwrap(),
|
|
||||||
};
|
|
||||||
let mut latest = Instant::now();
|
|
||||||
let mut step = Duration::MAX;
|
|
||||||
while !step.is_zero() {
|
|
||||||
if let Some(candidate) = latest.checked_add(step) {
|
|
||||||
latest = candidate;
|
|
||||||
}
|
|
||||||
step /= 2;
|
|
||||||
}
|
|
||||||
let opened_at = ClientTimestamp {
|
|
||||||
mono: latest,
|
|
||||||
wall: SystemTime::now(),
|
|
||||||
};
|
|
||||||
let machine = client
|
|
||||||
.runtime
|
|
||||||
.prepare_open_acceptance(reply, opened_at)
|
|
||||||
.unwrap();
|
|
||||||
let previous_recv_len = client.recv_buffer.len();
|
|
||||||
|
|
||||||
let failure = client.prepare_client_open(machine, opened_at).unwrap_err();
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
failure.primary,
|
|
||||||
ClientError::NegotiationRejected { .. }
|
|
||||||
));
|
|
||||||
assert!(failure.machine.cleanup_close_packet().is_some());
|
|
||||||
assert!(client.runtime.prepare_open_request().is_ok());
|
|
||||||
assert!(client.schedule.is_none());
|
|
||||||
assert_eq!(client.applied_traffic_class, None);
|
|
||||||
assert_eq!(client.recv_buffer.len(), previous_recv_len);
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn post_token_negotiation_failure_sends_cleanup_close() {
|
fn post_token_negotiation_failure_sends_cleanup_close() {
|
||||||
let mut returned = default_params();
|
let mut returned = default_params();
|
||||||
@@ -377,146 +137,143 @@ fn post_token_negotiation_failure_sends_cleanup_close() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cleanup_send_failure_does_not_replace_primary_open_error() {
|
fn successful_open_handshake() {
|
||||||
let mut returned = default_params();
|
let config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
||||||
returned.interval_ns += 1;
|
let params = params_from_config(&config).unwrap();
|
||||||
let server = start_fake_server(move |socket, tx| {
|
let server = open_success_server(params.clone());
|
||||||
let (_, peer) = recv_request(&socket, &tx);
|
|
||||||
socket
|
|
||||||
.send_to(
|
|
||||||
&open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, &returned, None),
|
|
||||||
peer,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
});
|
|
||||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
||||||
client.test_hooks.fail_cleanup_send.set(true);
|
|
||||||
|
|
||||||
assert!(matches!(
|
let negotiated = assert_open_started(client.open().unwrap());
|
||||||
client.open(),
|
assert_eq!(negotiated.params, params);
|
||||||
Err(ClientError::NegotiationRejected { .. })
|
|
||||||
));
|
assert_eq!(client.applied_traffic_class, Some(0));
|
||||||
assert!(client.runtime.prepare_open_request().is_ok());
|
|
||||||
server.join();
|
server.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn receive_buffer_reservation_failure_precedes_dscp_application() {
|
fn successful_open_restores_configured_receive_timeout() {
|
||||||
let mut returned = default_params();
|
let config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
||||||
// Raw wire TOS/Traffic Class byte for DSCP codepoint 46 (EF), matching
|
let params = params_from_config(&config).unwrap();
|
||||||
// what the client below requests so negotiation succeeds unchanged.
|
let server = open_success_server(params);
|
||||||
returned.dscp = 184;
|
|
||||||
let server = start_fake_server(move |socket, tx| {
|
|
||||||
let (_, peer) = recv_request(&socket, &tx);
|
|
||||||
socket
|
|
||||||
.send_to(
|
|
||||||
&open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, &returned, None),
|
|
||||||
peer,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let _ = recv_request(&socket, &tx);
|
|
||||||
});
|
|
||||||
let mut config = default_test_config(server.addr);
|
let mut config = default_test_config(server.addr);
|
||||||
config.dscp = 46;
|
config.socket_config.recv_timeout = Some(Duration::from_millis(75));
|
||||||
let mut client = Client::connect(config).unwrap();
|
let mut client = Client::connect(config).unwrap();
|
||||||
client
|
|
||||||
.test_hooks
|
|
||||||
.recv_buffer_len_override
|
|
||||||
.set(Some(usize::MAX));
|
|
||||||
client.test_hooks.fail_open_dscp.set(true);
|
|
||||||
|
|
||||||
assert!(matches!(
|
assert_open_started(client.open().unwrap());
|
||||||
client.open(),
|
|
||||||
Err(ClientError::AllocationFailed {
|
assert_eq!(
|
||||||
operation: "negotiated receive buffer",
|
client.socket.read_timeout().unwrap(),
|
||||||
..
|
Some(Duration::from_millis(75))
|
||||||
})
|
);
|
||||||
));
|
|
||||||
assert!(client.test_hooks.fail_open_dscp.get());
|
|
||||||
assert!(client.runtime.prepare_open_request().is_ok());
|
|
||||||
assert!(client.schedule.is_none());
|
|
||||||
assert_eq!(client.applied_traffic_class, None);
|
|
||||||
server.join();
|
server.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dscp_application_failure_leaves_machine_connected() {
|
fn open_timeout_after_all_timeouts() {
|
||||||
let params = default_params();
|
let server = timeout_server(Duration::from_millis(700));
|
||||||
let server = start_fake_server(move |socket, tx| {
|
let config = ClientConfig {
|
||||||
let (_, peer) = recv_request(&socket, &tx);
|
open_timeouts: vec![Duration::from_millis(200), Duration::from_millis(200)],
|
||||||
socket
|
socket_config: crate::SocketConfig {
|
||||||
.send_to(
|
recv_timeout: Some(Duration::from_millis(50)),
|
||||||
&open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None),
|
..Default::default()
|
||||||
peer,
|
},
|
||||||
)
|
..default_test_config(server.addr)
|
||||||
.unwrap();
|
};
|
||||||
let _ = recv_request(&socket, &tx);
|
let mut client = Client::connect(config).unwrap();
|
||||||
});
|
assert!(matches!(client.open(), Err(ClientError::OpenTimeout)));
|
||||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
assert_eq!(server.rx.iter().take(2).count(), 2);
|
||||||
client.test_hooks.fail_open_dscp.set(true);
|
assert_eq!(
|
||||||
|
client.socket.read_timeout().unwrap(),
|
||||||
assert!(matches!(
|
Some(Duration::from_millis(50))
|
||||||
client.open(),
|
);
|
||||||
Err(ClientError::SocketOption {
|
|
||||||
operation: "set negotiated DSCP",
|
|
||||||
..
|
|
||||||
})
|
|
||||||
));
|
|
||||||
assert!(client.test_hooks.prepared_active_session_before_dscp.get());
|
|
||||||
assert!(client.runtime.prepare_open_request().is_ok());
|
|
||||||
assert!(client.schedule.is_none());
|
|
||||||
assert_eq!(client.applied_traffic_class, None);
|
|
||||||
server.join();
|
server.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn read_timeout_restoration_failure_leaves_machine_connected() {
|
fn multiple_untrusted_datagrams_before_valid_reply_use_one_attempt() {
|
||||||
let params = default_params();
|
let params = default_params();
|
||||||
let server = start_fake_server(move |socket, tx| {
|
let server = start_fake_server(move |socket, tx| {
|
||||||
let (_, peer) = recv_request(&socket, &tx);
|
let (_, peer) = recv_request(&socket, &tx);
|
||||||
socket
|
let valid = open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None);
|
||||||
.send_to(
|
let mut wrong_type = valid.clone();
|
||||||
&open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None),
|
wrong_type[3] = FLAG_REPLY;
|
||||||
peer,
|
let mut reserved_flags = valid.clone();
|
||||||
)
|
reserved_flags[3] |= 0x10;
|
||||||
.unwrap();
|
let mut bad_magic = valid.clone();
|
||||||
let _ = recv_request(&socket, &tx);
|
bad_magic[0] ^= 0xff;
|
||||||
|
let unexpected_hmac =
|
||||||
|
open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, Some(b"unexpected"));
|
||||||
|
|
||||||
|
for packet in [
|
||||||
|
vec![0_u8],
|
||||||
|
bad_magic,
|
||||||
|
wrong_type,
|
||||||
|
reserved_flags,
|
||||||
|
unexpected_hmac,
|
||||||
|
valid,
|
||||||
|
] {
|
||||||
|
socket.send_to(&packet, peer).unwrap();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
||||||
client.test_hooks.fail_open_timeout_restore.set(true);
|
|
||||||
|
|
||||||
assert!(matches!(
|
assert_open_started(client.open().unwrap());
|
||||||
client.open(),
|
assert_eq!(server.rx.iter().take(1).count(), 1);
|
||||||
Err(ClientError::ReadTimeoutRestore { .. })
|
assert!(server.rx.try_recv().is_err());
|
||||||
));
|
|
||||||
assert!(client.runtime.prepare_open_request().is_ok());
|
|
||||||
assert!(client.schedule.is_none());
|
|
||||||
assert_eq!(client.applied_traffic_class, None);
|
|
||||||
server.join();
|
server.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dscp_rollback_failure_keeps_timeout_restoration_primary() {
|
fn only_ignored_open_datagrams_eventually_time_out() {
|
||||||
|
let server = start_fake_server(move |socket, tx| {
|
||||||
|
for _ in 0..2 {
|
||||||
|
let (_, peer) = recv_request(&socket, &tx);
|
||||||
|
socket.send_to(&[0_u8], peer).unwrap();
|
||||||
|
socket.send_to(&MAGIC, peer).unwrap();
|
||||||
|
}
|
||||||
|
thread::sleep(Duration::from_millis(300));
|
||||||
|
});
|
||||||
|
let config = ClientConfig {
|
||||||
|
open_timeouts: vec![Duration::from_millis(200), Duration::from_millis(200)],
|
||||||
|
socket_config: crate::SocketConfig {
|
||||||
|
recv_timeout: Some(Duration::from_millis(50)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..default_test_config(server.addr)
|
||||||
|
};
|
||||||
|
let mut client = Client::connect(config).unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(client.open(), Err(ClientError::OpenTimeout)));
|
||||||
|
assert_eq!(server.rx.iter().take(2).count(), 2);
|
||||||
|
assert!(server.rx.try_recv().is_err());
|
||||||
|
assert_eq!(
|
||||||
|
client.socket.read_timeout().unwrap(),
|
||||||
|
Some(Duration::from_millis(50))
|
||||||
|
);
|
||||||
|
server.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trusted_zero_token_normal_reply_is_terminal() {
|
||||||
let params = default_params();
|
let params = default_params();
|
||||||
let server = start_fake_server(move |socket, tx| {
|
let server = start_fake_server(move |socket, tx| {
|
||||||
let (_, peer) = recv_request(&socket, &tx);
|
let (_, peer) = recv_request(&socket, &tx);
|
||||||
socket
|
// encode_open_reply rejects a zero token without FLAG_CLOSE, so this
|
||||||
.send_to(
|
// deliberately non-compliant reply is built by encoding a normal
|
||||||
&open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None),
|
// reply with a placeholder token and then zeroing the token field.
|
||||||
peer,
|
let mut reply = open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None);
|
||||||
)
|
reply[HMAC_OFFSET..HMAC_OFFSET + 8].copy_from_slice(&0_u64.to_le_bytes());
|
||||||
.unwrap();
|
socket.send_to(&reply, peer).unwrap();
|
||||||
let _ = recv_request(&socket, &tx);
|
|
||||||
});
|
});
|
||||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
||||||
client.test_hooks.fail_open_timeout_restore.set(true);
|
|
||||||
client.test_hooks.fail_dscp_restore.set(true);
|
|
||||||
|
|
||||||
let error = client.open().unwrap_err();
|
assert!(matches!(
|
||||||
|
client.open(),
|
||||||
assert!(matches!(error, ClientError::ReadTimeoutRestore { .. }));
|
Err(ClientError::Protocol(irtt_proto::ProtoError::ZeroToken))
|
||||||
|
));
|
||||||
|
assert_eq!(server.rx.iter().take(1).count(), 1);
|
||||||
assert!(client.runtime.prepare_open_request().is_ok());
|
assert!(client.runtime.prepare_open_request().is_ok());
|
||||||
assert!(client.schedule.is_none());
|
|
||||||
assert_eq!(client.applied_traffic_class, None);
|
|
||||||
server.join();
|
server.join();
|
||||||
}
|
}
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
use super::*;
|
|
||||||
|
|
||||||
/// An interrupted receive (`io::ErrorKind::Interrupted`, e.g. `EINTR`) must be
|
|
||||||
/// invisible to the caller: retried transparently, with the eventual reply
|
|
||||||
/// classified exactly as if no interruption had occurred.
|
|
||||||
#[test]
|
|
||||||
fn interrupted_receive_is_retried_and_the_reply_is_still_classified() {
|
|
||||||
let params = default_params();
|
|
||||||
let server = start_fake_server(move |socket, tx| {
|
|
||||||
let (_, peer) = recv_request(&socket, &tx);
|
|
||||||
let reply = open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None);
|
|
||||||
socket.send_to(&reply, peer).unwrap();
|
|
||||||
|
|
||||||
let mut buf = [0_u8; 2048];
|
|
||||||
let (size, _) = socket.recv_from(&mut buf).unwrap();
|
|
||||||
let seq = u32::from_le_bytes(buf[12..16].try_into().unwrap());
|
|
||||||
let _ = size;
|
|
||||||
let ts = TimestampFields::default();
|
|
||||||
let reply_packet = echo_reply_packet(TOKEN, seq, ¶ms, &ts, None);
|
|
||||||
socket.send_to(&reply_packet, peer).unwrap();
|
|
||||||
});
|
|
||||||
let config = ClientConfig {
|
|
||||||
socket_config: crate::SocketConfig {
|
|
||||||
recv_timeout: Some(Duration::from_secs(2)),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..default_test_config(server.addr)
|
|
||||||
};
|
|
||||||
let mut client = Client::connect(config).unwrap();
|
|
||||||
assert_open_started(client.open().unwrap());
|
|
||||||
|
|
||||||
client.send_probe().unwrap();
|
|
||||||
thread::sleep(Duration::from_millis(50));
|
|
||||||
|
|
||||||
client.test_hooks.inject_recv_interrupted.set(10);
|
|
||||||
let events = client.recv_once().unwrap();
|
|
||||||
|
|
||||||
assert_eq!(events.len(), 1);
|
|
||||||
assert!(
|
|
||||||
matches!(&events[0], ClientEvent::EchoReply { seq: 0, .. }),
|
|
||||||
"expected a normal EchoReply despite interrupted receive attempts, got {:?}",
|
|
||||||
events[0]
|
|
||||||
);
|
|
||||||
|
|
||||||
client.close().unwrap();
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One logical receive keeps one timeout budget: repeated interruptions must
|
|
||||||
/// not extend a `recv_once` call past its configured receive timeout.
|
|
||||||
#[test]
|
|
||||||
fn interrupted_receive_does_not_extend_the_configured_timeout() {
|
|
||||||
let params = default_params();
|
|
||||||
let server = silent_open_server(params);
|
|
||||||
let config = ClientConfig {
|
|
||||||
socket_config: crate::SocketConfig {
|
|
||||||
recv_timeout: Some(Duration::from_millis(100)),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..default_test_config(server.addr)
|
|
||||||
};
|
|
||||||
let mut client = Client::connect(config).unwrap();
|
|
||||||
assert_open_started(client.open().unwrap());
|
|
||||||
|
|
||||||
// No reply is ever coming; only interrupted attempts occur before the
|
|
||||||
// real socket takes over and legitimately times out.
|
|
||||||
client.test_hooks.inject_recv_interrupted.set(20);
|
|
||||||
let started = Instant::now();
|
|
||||||
let events = client.recv_once().unwrap();
|
|
||||||
let elapsed = started.elapsed();
|
|
||||||
|
|
||||||
assert!(events.is_empty());
|
|
||||||
assert!(
|
|
||||||
elapsed < Duration::from_millis(500),
|
|
||||||
"interrupted receives must not extend the configured receive timeout, took {elapsed:?}"
|
|
||||||
);
|
|
||||||
|
|
||||||
client.close().unwrap();
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
@@ -20,9 +20,7 @@ fn kernel_rx_timestamp_unavailable_skip(test_name: &str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mod burst_recv;
|
mod burst_recv;
|
||||||
mod interrupted_receive;
|
|
||||||
mod packet_length;
|
mod packet_length;
|
||||||
mod probe_schedule;
|
|
||||||
mod reply_classification;
|
mod reply_classification;
|
||||||
mod reply_decode;
|
mod reply_decode;
|
||||||
mod reply_metadata;
|
mod reply_metadata;
|
||||||
|
|||||||
@@ -1,487 +0,0 @@
|
|||||||
use super::*;
|
|
||||||
|
|
||||||
fn probe_timestamps(permission_at: Instant, sent_at: ClientTimestamp) -> ProbeSendTimestamps {
|
|
||||||
ProbeSendTimestamps {
|
|
||||||
permission_at,
|
|
||||||
send_anchor: sent_at,
|
|
||||||
sent_at,
|
|
||||||
send_call_start: sent_at.mono,
|
|
||||||
send_finished_at: sent_at.mono,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn send_probe_fails_before_open() {
|
|
||||||
let server = start_fake_server(|_socket, _tx| {});
|
|
||||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
|
||||||
assert!(matches!(client.send_probe(), Err(ClientError::NotOpen)));
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn send_probe_sends_valid_echo_request() {
|
|
||||||
let params = default_params();
|
|
||||||
let server = silent_open_server(params.clone());
|
|
||||||
let config = ClientConfig {
|
|
||||||
socket_config: crate::SocketConfig {
|
|
||||||
recv_timeout: Some(Duration::from_millis(200)),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..default_test_config(server.addr)
|
|
||||||
};
|
|
||||||
let mut client = Client::connect(config).unwrap();
|
|
||||||
assert!(client.next_send_deadline().is_none());
|
|
||||||
assert_open_started(client.open().unwrap());
|
|
||||||
let events = client.send_probe().unwrap();
|
|
||||||
assert_eq!(events.len(), 1);
|
|
||||||
match &events[0] {
|
|
||||||
ClientEvent::EchoSent {
|
|
||||||
seq, remote, bytes, ..
|
|
||||||
} => {
|
|
||||||
assert_eq!(*seq, 0);
|
|
||||||
assert_eq!(*remote, server.addr);
|
|
||||||
assert_eq!(*bytes, echo_packet_len(false, ¶ms));
|
|
||||||
}
|
|
||||||
other => panic!("expected EchoSent, got {other:?}"),
|
|
||||||
}
|
|
||||||
thread::sleep(Duration::from_millis(30));
|
|
||||||
let packets: Vec<_> = server.rx.try_iter().collect();
|
|
||||||
let echo_reqs: Vec<_> = packets
|
|
||||||
.iter()
|
|
||||||
.filter(|p| p.len() >= 4 && p[3] & FLAG_OPEN == 0)
|
|
||||||
.collect();
|
|
||||||
let echo_req = echo_reqs.first().unwrap();
|
|
||||||
assert_eq!(&echo_req[..3], &MAGIC);
|
|
||||||
assert_eq!(echo_req[3], 0x00);
|
|
||||||
let req_token = u64::from_le_bytes(echo_req[4..12].try_into().unwrap());
|
|
||||||
assert_eq!(req_token, TOKEN);
|
|
||||||
let seq = u32::from_le_bytes(echo_req[12..16].try_into().unwrap());
|
|
||||||
assert_eq!(seq, 0);
|
|
||||||
client.close().unwrap();
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn blocking_short_probe_commits_before_presentation_length_error() {
|
|
||||||
let params = default_params();
|
|
||||||
let server = silent_open_server(params.clone());
|
|
||||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
|
||||||
assert_open_started(client.open().unwrap());
|
|
||||||
|
|
||||||
let expected = echo_packet_len(false, ¶ms);
|
|
||||||
let initial_deadline = client.next_send_deadline().unwrap();
|
|
||||||
client.probe_reported_len = Some(expected - 1);
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
client.send_probe(),
|
|
||||||
Err(ClientError::DatagramLengthMismatch {
|
|
||||||
expected: error_expected,
|
|
||||||
actual,
|
|
||||||
}) if error_expected == expected && actual + 1 == expected
|
|
||||||
));
|
|
||||||
assert_eq!(client.runtime.packets_sent(), 1);
|
|
||||||
assert_eq!(
|
|
||||||
client.next_send_deadline(),
|
|
||||||
Some(initial_deadline + client.runtime.config().interval)
|
|
||||||
);
|
|
||||||
|
|
||||||
client.close().unwrap();
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn blocking_failed_probe_send_preserves_machine_and_schedule() {
|
|
||||||
let params = default_params();
|
|
||||||
let server = silent_open_server(params);
|
|
||||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
|
||||||
assert_open_started(client.open().unwrap());
|
|
||||||
|
|
||||||
let initial_deadline = client.next_send_deadline().unwrap();
|
|
||||||
client.probe_send_error = true;
|
|
||||||
|
|
||||||
assert!(matches!(client.send_probe(), Err(ClientError::Socket(_))));
|
|
||||||
assert_eq!(client.runtime.packets_sent(), 0);
|
|
||||||
assert_eq!(client.next_send_deadline(), Some(initial_deadline));
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
client.send_probe().unwrap().as_slice(),
|
|
||||||
[ClientEvent::EchoSent { seq: 0, .. }]
|
|
||||||
));
|
|
||||||
|
|
||||||
client.close().unwrap();
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn blocking_timeout_finalization_overflow_does_not_send_or_commit() {
|
|
||||||
let params = default_params();
|
|
||||||
let server = silent_open_server(params);
|
|
||||||
let config = ClientConfig {
|
|
||||||
probe_timeout: Duration::MAX,
|
|
||||||
..default_test_config(server.addr)
|
|
||||||
};
|
|
||||||
let mut client = Client::connect(config).unwrap();
|
|
||||||
assert_open_started(client.open().unwrap());
|
|
||||||
|
|
||||||
let initial_deadline = client.next_send_deadline().unwrap();
|
|
||||||
let sent_at = ClientTimestamp {
|
|
||||||
mono: Instant::now(),
|
|
||||||
wall: SystemTime::now(),
|
|
||||||
};
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
client.send_probe_at(probe_timestamps(initial_deadline, sent_at)),
|
|
||||||
Err(ClientError::DurationOverflow)
|
|
||||||
));
|
|
||||||
assert_eq!(client.runtime.packets_sent(), 0);
|
|
||||||
assert_eq!(client.next_send_deadline(), Some(initial_deadline));
|
|
||||||
thread::sleep(Duration::from_millis(30));
|
|
||||||
assert!(server
|
|
||||||
.rx
|
|
||||||
.try_iter()
|
|
||||||
.all(|packet| packet.len() >= 4 && packet[3] & FLAG_OPEN != 0));
|
|
||||||
|
|
||||||
client.close().unwrap();
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn blocking_probe_separates_permission_committed_send_and_send_call_timing() {
|
|
||||||
let duration = Duration::from_secs(1);
|
|
||||||
let interval = Duration::from_millis(100);
|
|
||||||
let params = Params {
|
|
||||||
duration_ns: 1_000_000_000,
|
|
||||||
interval_ns: 100_000_000,
|
|
||||||
..default_params()
|
|
||||||
};
|
|
||||||
let server = silent_open_server(params.clone());
|
|
||||||
let config = ClientConfig {
|
|
||||||
duration: Some(duration),
|
|
||||||
interval,
|
|
||||||
..default_test_config(server.addr)
|
|
||||||
};
|
|
||||||
let mut client = Client::connect(config).unwrap();
|
|
||||||
assert_open_started(client.open().unwrap());
|
|
||||||
|
|
||||||
let opened_mono = Instant::now()
|
|
||||||
.checked_sub(Duration::from_secs(2))
|
|
||||||
.expect("test host monotonic clock has at least two seconds of history");
|
|
||||||
client.schedule = Some(
|
|
||||||
ProbeSchedule::new(
|
|
||||||
opened_mono,
|
|
||||||
&NegotiatedParams {
|
|
||||||
params,
|
|
||||||
restrictions: Vec::new(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
let permission_at = opened_mono + Duration::from_millis(500);
|
|
||||||
// Distinct pre-send anchor and post-send `sent_at`: the anchor drives
|
|
||||||
// the operational timeout deadline, while `sent_at` (captured after the
|
|
||||||
// send call) is the public measurement/timer_error endpoint. They are
|
|
||||||
// deliberately decoupled by this PR.
|
|
||||||
let send_anchor = ClientTimestamp {
|
|
||||||
wall: SystemTime::now(),
|
|
||||||
mono: opened_mono + Duration::from_millis(525),
|
|
||||||
};
|
|
||||||
let sent_at = ClientTimestamp {
|
|
||||||
wall: SystemTime::now(),
|
|
||||||
mono: opened_mono + Duration::from_millis(534),
|
|
||||||
};
|
|
||||||
let timestamps = ProbeSendTimestamps {
|
|
||||||
permission_at,
|
|
||||||
send_anchor,
|
|
||||||
sent_at,
|
|
||||||
send_call_start: opened_mono + Duration::from_millis(526),
|
|
||||||
send_finished_at: opened_mono + Duration::from_millis(533),
|
|
||||||
};
|
|
||||||
|
|
||||||
let events = client.send_probe_at(timestamps).unwrap();
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
events.as_slice(),
|
|
||||||
[ClientEvent::EchoSent {
|
|
||||||
scheduled_at,
|
|
||||||
sent_at: event_sent_at,
|
|
||||||
send_call,
|
|
||||||
timer_error,
|
|
||||||
..
|
|
||||||
}] if *scheduled_at == opened_mono
|
|
||||||
&& *event_sent_at == sent_at
|
|
||||||
&& *send_call == Duration::from_millis(7)
|
|
||||||
&& *timer_error == Duration::from_millis(534)
|
|
||||||
));
|
|
||||||
assert_eq!(client.next_send_deadline(), Some(opened_mono + interval));
|
|
||||||
assert_eq!(client.runtime.packets_sent(), 1);
|
|
||||||
// The timeout deadline is anchored to the pre-send `send_anchor`, not
|
|
||||||
// to the post-send `sent_at` captured 9ms later.
|
|
||||||
let timeout_at = send_anchor.mono + client.probe_timeout();
|
|
||||||
assert_ne!(timeout_at, sent_at.mono + client.probe_timeout());
|
|
||||||
assert!(client
|
|
||||||
.poll_timeouts_at(timeout_at - Duration::from_nanos(1))
|
|
||||||
.unwrap()
|
|
||||||
.is_empty());
|
|
||||||
assert!(matches!(
|
|
||||||
client.poll_timeouts_at(timeout_at).unwrap().as_slice(),
|
|
||||||
[ClientEvent::EchoLoss {
|
|
||||||
sent_at: loss_sent_at,
|
|
||||||
..
|
|
||||||
}] if *loss_sent_at == sent_at
|
|
||||||
));
|
|
||||||
|
|
||||||
client.close().unwrap();
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn blocking_send_call_excludes_wrapped_history_cleanup() {
|
|
||||||
let params = default_params();
|
|
||||||
let server = silent_open_server(params);
|
|
||||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
|
||||||
assert_open_started(client.open().unwrap());
|
|
||||||
|
|
||||||
let scheduled_at = client.next_send_deadline().unwrap();
|
|
||||||
let sent_at = ClientTimestamp {
|
|
||||||
wall: SystemTime::now(),
|
|
||||||
mono: scheduled_at + Duration::from_millis(5),
|
|
||||||
};
|
|
||||||
client.runtime.seed_wrapped_probe_history_for_test(sent_at);
|
|
||||||
let timestamps = ProbeSendTimestamps {
|
|
||||||
permission_at: scheduled_at,
|
|
||||||
send_anchor: sent_at,
|
|
||||||
sent_at,
|
|
||||||
send_call_start: scheduled_at + Duration::from_millis(6),
|
|
||||||
send_finished_at: scheduled_at + Duration::from_millis(13),
|
|
||||||
};
|
|
||||||
|
|
||||||
let events = client.send_probe_at(timestamps).unwrap();
|
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
events.as_slice(),
|
|
||||||
[ClientEvent::EchoSent {
|
|
||||||
seq: 0,
|
|
||||||
send_call,
|
|
||||||
..
|
|
||||||
}] if *send_call == Duration::from_millis(7)
|
|
||||||
));
|
|
||||||
assert!(!client.runtime.has_timed_out_metadata());
|
|
||||||
|
|
||||||
client.close().unwrap();
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn send_probe_respects_finite_duration_exclusive_end() {
|
|
||||||
let params = Params {
|
|
||||||
protocol_version: 1,
|
|
||||||
duration_ns: 1_000_000_000,
|
|
||||||
interval_ns: 500_000_000,
|
|
||||||
received_stats: ReceivedStats::Both,
|
|
||||||
stamp_at: StampAt::Both,
|
|
||||||
clock: Clock::Both,
|
|
||||||
..Params::default()
|
|
||||||
};
|
|
||||||
let server = silent_open_server(params.clone());
|
|
||||||
let config = ClientConfig {
|
|
||||||
duration: Some(Duration::from_secs(1)),
|
|
||||||
interval: Duration::from_millis(500),
|
|
||||||
socket_config: crate::SocketConfig {
|
|
||||||
recv_timeout: Some(Duration::from_millis(200)),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..default_test_config(server.addr)
|
|
||||||
};
|
|
||||||
let mut client = Client::connect(config).unwrap();
|
|
||||||
assert_open_started(client.open().unwrap());
|
|
||||||
|
|
||||||
let start = client.next_send_deadline().unwrap();
|
|
||||||
let interval = Duration::from_millis(500);
|
|
||||||
|
|
||||||
let now0 = ClientTimestamp {
|
|
||||||
mono: start,
|
|
||||||
wall: SystemTime::now(),
|
|
||||||
};
|
|
||||||
assert!(client
|
|
||||||
.send_probe_at(probe_timestamps(now0.mono, now0))
|
|
||||||
.is_ok());
|
|
||||||
|
|
||||||
let now1 = ClientTimestamp {
|
|
||||||
mono: start + interval,
|
|
||||||
wall: SystemTime::now(),
|
|
||||||
};
|
|
||||||
assert!(client
|
|
||||||
.send_probe_at(probe_timestamps(now1.mono, now1))
|
|
||||||
.is_ok());
|
|
||||||
|
|
||||||
let now2 = ClientTimestamp {
|
|
||||||
mono: start + Duration::from_secs(1),
|
|
||||||
wall: SystemTime::now(),
|
|
||||||
};
|
|
||||||
let events = client
|
|
||||||
.send_probe_at(probe_timestamps(now2.mono, now2))
|
|
||||||
.unwrap();
|
|
||||||
assert!(events.is_empty());
|
|
||||||
assert!(client.next_send_deadline().is_none());
|
|
||||||
|
|
||||||
client.close().unwrap();
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn managed_probe_skips_missed_schedule_slots() {
|
|
||||||
let interval = Duration::from_millis(10);
|
|
||||||
let params = Params {
|
|
||||||
protocol_version: 1,
|
|
||||||
duration_ns: 0,
|
|
||||||
interval_ns: 10_000_000,
|
|
||||||
received_stats: ReceivedStats::Both,
|
|
||||||
stamp_at: StampAt::Both,
|
|
||||||
clock: Clock::Both,
|
|
||||||
..Params::default()
|
|
||||||
};
|
|
||||||
let server = echo_server(params);
|
|
||||||
let config = ClientConfig {
|
|
||||||
duration: None,
|
|
||||||
interval,
|
|
||||||
..default_test_config(server.addr)
|
|
||||||
};
|
|
||||||
let mut client = Client::connect(config).unwrap();
|
|
||||||
assert_open_started(client.open().unwrap());
|
|
||||||
|
|
||||||
let first_deadline = client.next_send_deadline().unwrap();
|
|
||||||
let permission_at = first_deadline + Duration::from_millis(45);
|
|
||||||
let delayed_send = ClientTimestamp {
|
|
||||||
mono: first_deadline + Duration::from_millis(48),
|
|
||||||
wall: SystemTime::now(),
|
|
||||||
};
|
|
||||||
let timestamps = ProbeSendTimestamps {
|
|
||||||
permission_at,
|
|
||||||
send_anchor: delayed_send,
|
|
||||||
sent_at: delayed_send,
|
|
||||||
send_call_start: first_deadline + Duration::from_millis(49),
|
|
||||||
send_finished_at: first_deadline + Duration::from_millis(55),
|
|
||||||
};
|
|
||||||
let events = client
|
|
||||||
.send_managed_probe_at(first_deadline, timestamps)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(events.len(), 1);
|
|
||||||
assert!(matches!(
|
|
||||||
events[0],
|
|
||||||
ClientEvent::EchoSent {
|
|
||||||
seq: 0,
|
|
||||||
scheduled_at,
|
|
||||||
sent_at,
|
|
||||||
send_call,
|
|
||||||
timer_error,
|
|
||||||
..
|
|
||||||
} if scheduled_at == first_deadline + Duration::from_millis(40)
|
|
||||||
&& sent_at == delayed_send
|
|
||||||
&& send_call == Duration::from_millis(6)
|
|
||||||
&& timer_error == Duration::from_millis(8)
|
|
||||||
));
|
|
||||||
assert_eq!(
|
|
||||||
client.next_send_deadline(),
|
|
||||||
Some(first_deadline + Duration::from_millis(50))
|
|
||||||
);
|
|
||||||
|
|
||||||
client.close().unwrap();
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn managed_probe_skip_preserves_finite_run_end() {
|
|
||||||
let interval = Duration::from_millis(10);
|
|
||||||
let duration = Duration::from_millis(50);
|
|
||||||
let params = Params {
|
|
||||||
protocol_version: 1,
|
|
||||||
duration_ns: 50_000_000,
|
|
||||||
interval_ns: 10_000_000,
|
|
||||||
received_stats: ReceivedStats::Both,
|
|
||||||
stamp_at: StampAt::Both,
|
|
||||||
clock: Clock::Both,
|
|
||||||
..Params::default()
|
|
||||||
};
|
|
||||||
let server = echo_server(params);
|
|
||||||
let config = ClientConfig {
|
|
||||||
duration: Some(duration),
|
|
||||||
interval,
|
|
||||||
..default_test_config(server.addr)
|
|
||||||
};
|
|
||||||
let mut client = Client::connect(config).unwrap();
|
|
||||||
assert_open_started(client.open().unwrap());
|
|
||||||
|
|
||||||
let first_deadline = client.next_send_deadline().unwrap();
|
|
||||||
let delayed_send = ClientTimestamp {
|
|
||||||
mono: first_deadline + Duration::from_millis(45),
|
|
||||||
wall: SystemTime::now(),
|
|
||||||
};
|
|
||||||
let first = client
|
|
||||||
.send_managed_probe_at(
|
|
||||||
first_deadline,
|
|
||||||
probe_timestamps(delayed_send.mono, delayed_send),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let second_sent_at = ClientTimestamp {
|
|
||||||
mono: first_deadline + Duration::from_millis(46),
|
|
||||||
wall: SystemTime::now(),
|
|
||||||
};
|
|
||||||
let second = client
|
|
||||||
.send_managed_probe_at(
|
|
||||||
first_deadline + duration,
|
|
||||||
probe_timestamps(second_sent_at.mono, second_sent_at),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(first.len(), 1);
|
|
||||||
assert!(second.is_empty());
|
|
||||||
assert!(client.next_send_deadline().is_none());
|
|
||||||
|
|
||||||
client.close().unwrap();
|
|
||||||
server.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn receive_wait_is_bounded_by_next_ten_millisecond_deadline() {
|
|
||||||
let now = Instant::now();
|
|
||||||
let deadline = now + Duration::from_millis(10);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
bounded_receive_timeout(
|
|
||||||
Some(deadline),
|
|
||||||
Some(Duration::from_millis(20)),
|
|
||||||
Duration::from_millis(20),
|
|
||||||
now,
|
|
||||||
),
|
|
||||||
Some(Duration::from_millis(10))
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
bounded_receive_timeout(
|
|
||||||
Some(now),
|
|
||||||
Some(Duration::from_millis(20)),
|
|
||||||
Duration::from_millis(20),
|
|
||||||
now,
|
|
||||||
),
|
|
||||||
None
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn connect_rejects_invalid_probe_limits() {
|
|
||||||
for config in [
|
|
||||||
ClientConfig {
|
|
||||||
max_pending_probes: 0,
|
|
||||||
..ClientConfig::default()
|
|
||||||
},
|
|
||||||
ClientConfig {
|
|
||||||
probe_timeout: Duration::ZERO,
|
|
||||||
..ClientConfig::default()
|
|
||||||
},
|
|
||||||
] {
|
|
||||||
assert!(matches!(
|
|
||||||
Client::connect(config),
|
|
||||||
Err(ClientError::InvalidConfig { .. })
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -85,12 +85,18 @@ pub struct ClientConfig {
|
|||||||
/// `None` requests continuous mode and is encoded on the wire as a zero
|
/// `None` requests continuous mode and is encoded on the wire as a zero
|
||||||
/// duration. Use [`RunMode::NoTest`] when the caller wants negotiation only
|
/// duration. Use [`RunMode::NoTest`] when the caller wants negotiation only
|
||||||
/// without sending probes.
|
/// without sending probes.
|
||||||
|
///
|
||||||
|
/// The managed driver stops sending after the negotiated duration.
|
||||||
|
/// Low-level adapters leave run duration to the caller.
|
||||||
pub duration: Option<Duration>,
|
pub duration: Option<Duration>,
|
||||||
/// Requested spacing between probe sends.
|
/// Requested spacing between probe sends.
|
||||||
///
|
///
|
||||||
/// The interval is encoded as nanoseconds in the open request and must be
|
/// The interval is encoded as nanoseconds in the open request and must be
|
||||||
/// greater than zero. The server may return a different interval depending
|
/// greater than zero. The server may return a different interval depending
|
||||||
/// on the negotiated policy and server restrictions.
|
/// on the negotiated policy and server restrictions.
|
||||||
|
///
|
||||||
|
/// The managed driver uses the negotiated interval for cadence.
|
||||||
|
/// Low-level adapters send whenever called.
|
||||||
pub interval: Duration,
|
pub interval: Duration,
|
||||||
/// Requested echo packet payload length, in bytes.
|
/// Requested echo packet payload length, in bytes.
|
||||||
///
|
///
|
||||||
@@ -273,7 +279,8 @@ pub enum NegotiationPolicy {
|
|||||||
/// Mode requested during the IRTT open exchange.
|
/// Mode requested during the IRTT open exchange.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum RunMode {
|
pub enum RunMode {
|
||||||
/// Open a session and send echo probes for the negotiated run duration.
|
/// Open a probe session. The managed driver runs for the negotiated duration;
|
||||||
|
/// low-level adapters leave sending and stopping to the caller.
|
||||||
Normal,
|
Normal,
|
||||||
/// Complete negotiation without running the echo probe test.
|
/// Complete negotiation without running the echo probe test.
|
||||||
NoTest,
|
NoTest,
|
||||||
|
|||||||
@@ -92,8 +92,9 @@ pub enum ClientEvent {
|
|||||||
seq: u32,
|
seq: u32,
|
||||||
/// Resolved remote socket address.
|
/// Resolved remote socket address.
|
||||||
remote: SocketAddr,
|
remote: SocketAddr,
|
||||||
/// Monotonic deadline at which the probe was scheduled to be sent.
|
/// Intended send deadline, supplied by the managed runner.
|
||||||
scheduled_at: Instant,
|
/// Low-level adapters leave this absent.
|
||||||
|
scheduled_at: Option<Instant>,
|
||||||
/// Paired client wall/monotonic timestamp captured immediately after
|
/// Paired client wall/monotonic timestamp captured immediately after
|
||||||
/// the successful UDP send completed. This is the RTT and userspace
|
/// the successful UDP send completed. This is the RTT and userspace
|
||||||
/// upstream one-way-delay fallback endpoint. The operational probe
|
/// upstream one-way-delay fallback endpoint. The operational probe
|
||||||
@@ -106,8 +107,8 @@ pub enum ClientEvent {
|
|||||||
/// scheduler commit work.
|
/// scheduler commit work.
|
||||||
send_call: Duration,
|
send_call: Duration,
|
||||||
/// Absolute difference between the scheduled send time and the
|
/// Absolute difference between the scheduled send time and the
|
||||||
/// post-send `sent_at` timestamp above.
|
/// post-send `sent_at` timestamp above; absent for low-level sends.
|
||||||
timer_error: Duration,
|
timer_error: Option<Duration>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// First in-window reply for a pending echo request.
|
/// First in-window reply for a pending echo request.
|
||||||
|
|||||||
@@ -18,3 +18,6 @@ pub use task::*;
|
|||||||
|
|
||||||
#[cfg(all(feature = "tokio", test))]
|
#[cfg(all(feature = "tokio", test))]
|
||||||
mod blocking_tests;
|
mod blocking_tests;
|
||||||
|
|
||||||
|
#[cfg(feature = "tokio")]
|
||||||
|
mod schedule;
|
||||||
+43
-133
@@ -4,7 +4,6 @@ use crate::{error::ClientError, session::NegotiatedParams};
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct ProbeSchedule {
|
pub(crate) struct ProbeSchedule {
|
||||||
start_at: Instant,
|
|
||||||
end_at: Option<Instant>,
|
end_at: Option<Instant>,
|
||||||
interval: Duration,
|
interval: Duration,
|
||||||
next_send_at: Option<Instant>,
|
next_send_at: Option<Instant>,
|
||||||
@@ -39,7 +38,6 @@ impl ProbeSchedule {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
start_at,
|
|
||||||
end_at,
|
end_at,
|
||||||
interval,
|
interval,
|
||||||
next_send_at: Some(start_at),
|
next_send_at: Some(start_at),
|
||||||
@@ -50,7 +48,6 @@ impl ProbeSchedule {
|
|||||||
self.next_send_at
|
self.next_send_at
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "tokio")]
|
|
||||||
pub(crate) fn interval(&self) -> Duration {
|
pub(crate) fn interval(&self) -> Duration {
|
||||||
self.interval
|
self.interval
|
||||||
}
|
}
|
||||||
@@ -66,26 +63,12 @@ impl ProbeSchedule {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn preflight_caller_commit(
|
|
||||||
&self,
|
|
||||||
next_packets_sent: u64,
|
|
||||||
) -> Result<ScheduleCommit, ClientError> {
|
|
||||||
let scheduled_at = self
|
|
||||||
.next_send_at
|
|
||||||
.expect("a caller schedule commit requires a permitted probe");
|
|
||||||
let interval_ns =
|
|
||||||
u64::try_from(self.interval.as_nanos()).map_err(|_| ClientError::DurationOverflow)?;
|
|
||||||
let next_send_at = next_probe_deadline(self.start_at, interval_ns, next_packets_sent)?;
|
|
||||||
Ok(self.finish_commit(scheduled_at, next_send_at))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(feature = "tokio", test))]
|
|
||||||
pub(crate) fn preflight_managed_commit(
|
pub(crate) fn preflight_managed_commit(
|
||||||
&self,
|
&self,
|
||||||
scheduled_at: Instant,
|
scheduled_at: Instant,
|
||||||
permission_at: Instant,
|
permission_at: Instant,
|
||||||
) -> Result<ScheduleCommit, ClientError> {
|
) -> Result<ScheduleCommit, ClientError> {
|
||||||
let (scheduled_at, next_send_at, _) =
|
let (scheduled_at, next_send_at) =
|
||||||
advance_cadence(scheduled_at, self.interval, permission_at)?;
|
advance_cadence(scheduled_at, self.interval, permission_at)?;
|
||||||
Ok(self.finish_commit(scheduled_at, next_send_at))
|
Ok(self.finish_commit(scheduled_at, next_send_at))
|
||||||
}
|
}
|
||||||
@@ -94,6 +77,16 @@ impl ProbeSchedule {
|
|||||||
self.next_send_at = commit.next_send_at;
|
self.next_send_at = commit.next_send_at;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reclaim slots crossed while a successful socket submission was in flight.
|
||||||
|
/// This follows the send commit; it never advances a failed submission.
|
||||||
|
pub(crate) fn skip_missed_slots_at(&mut self, now: Instant) -> Result<(), ClientError> {
|
||||||
|
if let Some(deadline) = self.next_send_at.filter(|deadline| *deadline <= now) {
|
||||||
|
let commit = self.preflight_managed_commit(deadline, now)?;
|
||||||
|
self.commit(commit);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn is_finished(&self) -> bool {
|
pub(crate) fn is_finished(&self) -> bool {
|
||||||
self.next_send_at.is_none()
|
self.next_send_at.is_none()
|
||||||
}
|
}
|
||||||
@@ -110,25 +103,11 @@ impl ProbeSchedule {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_probe_deadline(
|
|
||||||
start: Instant,
|
|
||||||
interval_ns: u64,
|
|
||||||
packets_sent: u64,
|
|
||||||
) -> Result<Instant, ClientError> {
|
|
||||||
let offset_ns = interval_ns
|
|
||||||
.checked_mul(packets_sent)
|
|
||||||
.ok_or(ClientError::DurationOverflow)?;
|
|
||||||
start
|
|
||||||
.checked_add(Duration::from_nanos(offset_ns))
|
|
||||||
.ok_or(ClientError::DurationOverflow)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(feature = "tokio", test))]
|
|
||||||
pub(crate) fn advance_cadence(
|
pub(crate) fn advance_cadence(
|
||||||
deadline: Instant,
|
deadline: Instant,
|
||||||
interval: Duration,
|
interval: Duration,
|
||||||
now: Instant,
|
now: Instant,
|
||||||
) -> Result<(Instant, Instant, u128), ClientError> {
|
) -> Result<(Instant, Instant), ClientError> {
|
||||||
if interval.is_zero() {
|
if interval.is_zero() {
|
||||||
return Err(ClientError::InvalidConfig {
|
return Err(ClientError::InvalidConfig {
|
||||||
reason: "probe interval must be greater than zero".to_owned(),
|
reason: "probe interval must be greater than zero".to_owned(),
|
||||||
@@ -160,10 +139,9 @@ pub(crate) fn advance_cadence(
|
|||||||
let next_at = deadline
|
let next_at = deadline
|
||||||
.checked_add(next_offset)
|
.checked_add(next_offset)
|
||||||
.ok_or(ClientError::DurationOverflow)?;
|
.ok_or(ClientError::DurationOverflow)?;
|
||||||
Ok((scheduled_at, next_at, elapsed_slots))
|
Ok((scheduled_at, next_at))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "tokio", test))]
|
|
||||||
fn duration_from_nanos(nanos: u128) -> Result<Duration, ClientError> {
|
fn duration_from_nanos(nanos: u128) -> Result<Duration, ClientError> {
|
||||||
const NANOS_PER_SECOND: u128 = 1_000_000_000;
|
const NANOS_PER_SECOND: u128 = 1_000_000_000;
|
||||||
let seconds =
|
let seconds =
|
||||||
@@ -178,16 +156,15 @@ pub(crate) fn instant_abs_diff(left: Instant, right: Instant) -> Duration {
|
|||||||
.or_else(|| right.checked_duration_since(left))
|
.or_else(|| right.checked_duration_since(left))
|
||||||
.unwrap_or(Duration::ZERO)
|
.unwrap_or(Duration::ZERO)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use irtt_proto::Params;
|
use irtt_proto::Params;
|
||||||
|
|
||||||
fn negotiated(interval_ns: i64, duration_ns: i64) -> NegotiatedParams {
|
fn negotiated(duration_ns: i64) -> NegotiatedParams {
|
||||||
NegotiatedParams {
|
NegotiatedParams {
|
||||||
params: Params {
|
params: Params {
|
||||||
interval_ns,
|
interval_ns: 10_000_000,
|
||||||
duration_ns,
|
duration_ns,
|
||||||
..Params::default()
|
..Params::default()
|
||||||
},
|
},
|
||||||
@@ -196,123 +173,56 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn initial_deadline_is_the_opening_time() {
|
fn missed_slots_skip_on_the_absolute_grid_only_after_commit() {
|
||||||
let opened_at = Instant::now();
|
|
||||||
|
|
||||||
let schedule = ProbeSchedule::new(opened_at, &negotiated(10_000_000, 0)).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(schedule.next_send_deadline(), Some(opened_at));
|
|
||||||
assert!(!schedule.is_finished());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn caller_preflight_keeps_absolute_cadence_without_mutation() {
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let schedule = ProbeSchedule::new(start, &negotiated(10_000_000, 0)).unwrap();
|
let mut schedule = ProbeSchedule::new(start, &negotiated(0)).unwrap();
|
||||||
|
for (now_ms, intended_ms, next_ms) in [(0, 0, 10), (45, 40, 50), (50, 50, 60)] {
|
||||||
let commit = schedule.preflight_caller_commit(3).unwrap();
|
let deadline = schedule.next_send_deadline().unwrap();
|
||||||
|
let commit = schedule
|
||||||
assert_eq!(commit.scheduled_at, start);
|
.preflight_managed_commit(deadline, start + Duration::from_millis(now_ms))
|
||||||
assert_eq!(commit.next_send_at, Some(start + Duration::from_millis(30)));
|
.unwrap();
|
||||||
assert_eq!(schedule.next_send_deadline(), Some(start));
|
assert_eq!(
|
||||||
}
|
commit.scheduled_at,
|
||||||
|
start + Duration::from_millis(intended_ms)
|
||||||
#[test]
|
);
|
||||||
fn caller_commit_advances_exactly_one_cadence_slot() {
|
// A failed or pending submission discards this preflight.
|
||||||
let start = Instant::now();
|
assert_eq!(schedule.next_send_deadline(), Some(deadline));
|
||||||
let mut schedule = ProbeSchedule::new(start, &negotiated(10_000_000, 0)).unwrap();
|
|
||||||
|
|
||||||
let commit = schedule.preflight_caller_commit(1).unwrap();
|
|
||||||
assert_eq!(schedule.next_send_deadline(), Some(start));
|
|
||||||
|
|
||||||
schedule.commit(commit);
|
schedule.commit(commit);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
schedule.next_send_deadline(),
|
schedule.next_send_deadline(),
|
||||||
Some(start + Duration::from_millis(10))
|
Some(start + Duration::from_millis(next_ms))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
schedule
|
||||||
#[test]
|
.skip_missed_slots_at(start + Duration::from_millis(85))
|
||||||
fn finite_duration_uses_an_exclusive_end_boundary() {
|
.unwrap();
|
||||||
let start = Instant::now();
|
|
||||||
let mut schedule = ProbeSchedule::new(start, &negotiated(10_000_000, 20_000_000)).unwrap();
|
|
||||||
|
|
||||||
let first = schedule.preflight_caller_commit(1).unwrap();
|
|
||||||
schedule.commit(first);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
schedule.next_send_deadline(),
|
schedule.next_send_deadline(),
|
||||||
Some(start + Duration::from_millis(10))
|
Some(start + Duration::from_millis(90))
|
||||||
);
|
);
|
||||||
|
|
||||||
let second = schedule.preflight_caller_commit(2).unwrap();
|
|
||||||
schedule.commit(second);
|
|
||||||
|
|
||||||
assert!(schedule.is_finished());
|
|
||||||
assert_eq!(schedule.next_send_deadline(), None);
|
|
||||||
assert!(!schedule.permit_probe_at(start + Duration::from_millis(20)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn permit_at_exclusive_end_finishes_schedule_without_a_send() {
|
fn finite_duration_excludes_its_end_even_when_a_slot_is_missed() {
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let mut schedule = ProbeSchedule::new(start, &negotiated(10_000_000, 20_000_000)).unwrap();
|
let mut schedule = ProbeSchedule::new(start, &negotiated(20_000_000)).unwrap();
|
||||||
|
assert!(schedule.permit_probe_at(start + Duration::from_millis(19)));
|
||||||
assert!(!schedule.permit_probe_at(start + Duration::from_millis(20)));
|
|
||||||
assert!(schedule.is_finished());
|
|
||||||
assert_eq!(schedule.next_send_deadline(), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn managed_preflight_skips_missed_slots_without_mutation() {
|
|
||||||
let start = Instant::now();
|
|
||||||
let schedule = ProbeSchedule::new(start, &negotiated(10_000_000, 0)).unwrap();
|
|
||||||
let sent_at = start + Duration::from_millis(45);
|
|
||||||
|
|
||||||
let commit = schedule.preflight_managed_commit(start, sent_at).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(commit.scheduled_at, start + Duration::from_millis(40));
|
|
||||||
assert_eq!(commit.next_send_at, Some(start + Duration::from_millis(50)));
|
|
||||||
assert_eq!(schedule.next_send_deadline(), Some(start));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn delayed_managed_commit_does_not_replay_catch_up_slots() {
|
|
||||||
let start = Instant::now();
|
|
||||||
let mut schedule = ProbeSchedule::new(start, &negotiated(10_000_000, 0)).unwrap();
|
|
||||||
let delayed_send = start + Duration::from_millis(45);
|
|
||||||
|
|
||||||
let commit = schedule
|
let commit = schedule
|
||||||
.preflight_managed_commit(start, delayed_send)
|
.preflight_managed_commit(start, start + Duration::from_millis(19))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
schedule.commit(commit);
|
schedule.commit(commit);
|
||||||
|
assert!(schedule.is_finished());
|
||||||
|
|
||||||
assert_eq!(
|
let mut missed = ProbeSchedule::new(start, &negotiated(20_000_000)).unwrap();
|
||||||
schedule.next_send_deadline(),
|
assert!(!missed.permit_probe_at(start + Duration::from_millis(20)));
|
||||||
Some(start + Duration::from_millis(50))
|
assert!(missed.is_finished());
|
||||||
);
|
|
||||||
assert!(schedule.next_send_deadline().unwrap() > delayed_send);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn deadline_arithmetic_overflow_is_detected_before_commit() {
|
fn cadence_rejects_overflow_before_commit() {
|
||||||
let start = Instant::now();
|
|
||||||
let schedule = ProbeSchedule::new(start, &negotiated(2, 0)).unwrap();
|
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
schedule.preflight_caller_commit(u64::MAX),
|
advance_cadence(Instant::now(), Duration::MAX, Instant::now()),
|
||||||
Err(ClientError::DurationOverflow)
|
Err(ClientError::DurationOverflow)
|
||||||
));
|
));
|
||||||
assert_eq!(schedule.next_send_deadline(), Some(start));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn timer_error_is_adapter_owned() {
|
|
||||||
let scheduled_at = Instant::now();
|
|
||||||
let sent_at = scheduled_at + Duration::from_millis(7);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
instant_abs_diff(sent_at, scheduled_at),
|
|
||||||
Duration::from_millis(7)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,6 +25,8 @@ use crate::{
|
|||||||
ClientConfig, ClientError, ClientEvent, OpenOutcome,
|
ClientConfig, ClientError, ClientEvent, OpenOutcome,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::schedule::{instant_abs_diff, ProbeSchedule};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
classify_client_error, ManagedClientConfig, ManagedCommandAcknowledgement,
|
classify_client_error, ManagedClientConfig, ManagedCommandAcknowledgement,
|
||||||
ManagedCommandApplyError, ManagedCommandError, ManagedCompletionPolicy, ManagedConfigError,
|
ManagedCommandApplyError, ManagedCommandError, ManagedCompletionPolicy, ManagedConfigError,
|
||||||
@@ -504,6 +506,7 @@ impl TargetCounters {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct TargetRuntime {
|
struct TargetRuntime {
|
||||||
|
schedule: Option<ProbeSchedule>,
|
||||||
instance: TargetInstance,
|
instance: TargetInstance,
|
||||||
config: ManagedTargetConfig,
|
config: ManagedTargetConfig,
|
||||||
desired: bool,
|
desired: bool,
|
||||||
@@ -991,6 +994,7 @@ impl ManagedClientTask {
|
|||||||
};
|
};
|
||||||
created_instances.push(instance.clone());
|
created_instances.push(instance.clone());
|
||||||
self.targets.push(TargetRuntime {
|
self.targets.push(TargetRuntime {
|
||||||
|
schedule: None,
|
||||||
instance,
|
instance,
|
||||||
server_addr: Arc::from(planned.target.server_addr.clone()),
|
server_addr: Arc::from(planned.target.server_addr.clone()),
|
||||||
config: planned.target,
|
config: planned.target,
|
||||||
@@ -1243,7 +1247,25 @@ impl ManagedClientTask {
|
|||||||
self.targets[index].state = TargetState::Opening { client, open };
|
self.targets[index].state = TargetState::Opening { client, open };
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
Poll::Ready(Ok(OpenOutcome::Started { event, .. })) => {
|
Poll::Ready(Ok(OpenOutcome::Started {
|
||||||
|
event, negotiated, ..
|
||||||
|
})) => {
|
||||||
|
let ClientEvent::SessionStarted { at: opened_at, .. } = &event else {
|
||||||
|
unreachable!("Started carries SessionStarted");
|
||||||
|
};
|
||||||
|
match ProbeSchedule::new(opened_at.mono, &negotiated) {
|
||||||
|
Ok(schedule) => self.targets[index].schedule = Some(schedule),
|
||||||
|
Err(error) => {
|
||||||
|
return self.begin_open_session_failure(
|
||||||
|
index,
|
||||||
|
client,
|
||||||
|
ManagedTargetFailurePhase::Timing,
|
||||||
|
error,
|
||||||
|
now,
|
||||||
|
OpenSessionFailureCleanup::Close,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
self.publish_client_events(index, vec![event]);
|
self.publish_client_events(index, vec![event]);
|
||||||
if self.state == DriverState::Stopping
|
if self.state == DriverState::Stopping
|
||||||
|| self.effective_retirement(index).is_some()
|
|| self.effective_retirement(index).is_some()
|
||||||
@@ -1340,7 +1362,15 @@ impl ManagedClientTask {
|
|||||||
self.finish_target(index, ManagedTargetEndReason::PeerClosed, None);
|
self.finish_target(index, ManagedTargetEndReason::PeerClosed, None);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if client.is_run_complete() {
|
if self.targets[index]
|
||||||
|
.schedule
|
||||||
|
.as_mut()
|
||||||
|
.is_some_and(|schedule| {
|
||||||
|
schedule.permit_probe_at(now);
|
||||||
|
schedule.is_finished()
|
||||||
|
})
|
||||||
|
&& !client.has_pending_probes()
|
||||||
|
{
|
||||||
self.targets[index].sync_packets_sent(&client);
|
self.targets[index].sync_packets_sent(&client);
|
||||||
return self.begin_drain(
|
return self.begin_drain(
|
||||||
index,
|
index,
|
||||||
@@ -1620,13 +1650,15 @@ impl ManagedClientTask {
|
|||||||
if !target.desired || target.retirement.is_some() {
|
if !target.desired || target.retirement.is_some() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let TargetState::Active { client } = &target.state else {
|
let TargetState::Active { .. } = &target.state else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
active += 1;
|
active += 1;
|
||||||
let interval = client
|
let interval = target
|
||||||
.probe_interval()
|
.schedule
|
||||||
.expect("active managed targets have a committed probe schedule");
|
.as_ref()
|
||||||
|
.expect("active managed targets have a probe schedule")
|
||||||
|
.interval();
|
||||||
minimum = minimum.into_iter().chain(Some(interval)).min();
|
minimum = minimum.into_iter().chain(Some(interval)).min();
|
||||||
}
|
}
|
||||||
minimum.map(|interval| (active, stagger_spacing(interval, active)))
|
minimum.map(|interval| (active, stagger_spacing(interval, active)))
|
||||||
@@ -1709,6 +1741,9 @@ impl ManagedClientTask {
|
|||||||
self.targets[index].state = state;
|
self.targets[index].state = state;
|
||||||
return SendResult::NotAttempted;
|
return SendResult::NotAttempted;
|
||||||
};
|
};
|
||||||
|
// A burst pass may span several slots; validate this target against
|
||||||
|
// fresh time rather than the pass's earlier scheduling snapshot.
|
||||||
|
let now = now.max(Instant::now());
|
||||||
if client
|
if client
|
||||||
.next_probe_timeout_deadline()
|
.next_probe_timeout_deadline()
|
||||||
.is_some_and(|deadline| deadline <= now)
|
.is_some_and(|deadline| deadline <= now)
|
||||||
@@ -1717,7 +1752,21 @@ impl ManagedClientTask {
|
|||||||
self.targets[index].state = TargetState::Active { client };
|
self.targets[index].state = TargetState::Active { client };
|
||||||
return SendResult::NotAttempted;
|
return SendResult::NotAttempted;
|
||||||
}
|
}
|
||||||
if client
|
let schedule = self.targets[index]
|
||||||
|
.schedule
|
||||||
|
.as_mut()
|
||||||
|
.expect("active managed targets have a probe schedule");
|
||||||
|
if !schedule.permit_probe_at(now) {
|
||||||
|
self.targets[index].send_waiting = false;
|
||||||
|
if client.has_pending_probes() {
|
||||||
|
self.targets[index].state = TargetState::Active { client };
|
||||||
|
return SendResult::NotAttempted;
|
||||||
|
}
|
||||||
|
self.targets[index].sync_packets_sent(&client);
|
||||||
|
self.begin_drain(index, client, ManagedTargetEndReason::TestComplete, now);
|
||||||
|
return SendResult::Ready { accepted: false };
|
||||||
|
}
|
||||||
|
if schedule
|
||||||
.next_send_deadline()
|
.next_send_deadline()
|
||||||
.is_none_or(|deadline| deadline > now)
|
.is_none_or(|deadline| deadline > now)
|
||||||
{
|
{
|
||||||
@@ -1725,8 +1774,25 @@ impl ManagedClientTask {
|
|||||||
self.targets[index].state = TargetState::Active { client };
|
self.targets[index].state = TargetState::Active { client };
|
||||||
return SendResult::NotAttempted;
|
return SendResult::NotAttempted;
|
||||||
}
|
}
|
||||||
|
let schedule = self.targets[index].schedule.as_ref().unwrap();
|
||||||
|
let commit =
|
||||||
|
match schedule.preflight_managed_commit(schedule.next_send_deadline().unwrap(), now) {
|
||||||
|
Ok(commit) => commit,
|
||||||
|
Err(error) => {
|
||||||
|
self.begin_open_session_failure(
|
||||||
|
index,
|
||||||
|
client,
|
||||||
|
ManagedTargetFailurePhase::Timing,
|
||||||
|
error,
|
||||||
|
now,
|
||||||
|
OpenSessionFailureCleanup::Drain,
|
||||||
|
);
|
||||||
|
return SendResult::Failed { accepted: false };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let scheduled_at = commit.scheduled_at;
|
||||||
let before = client.packets_sent();
|
let before = client.packets_sent();
|
||||||
let result = client.poll_send_probe(cx);
|
let mut result = client.poll_send_probe(cx);
|
||||||
let after = client.packets_sent();
|
let after = client.packets_sent();
|
||||||
let accepted = after > before;
|
let accepted = after > before;
|
||||||
let send_result = match &result {
|
let send_result = match &result {
|
||||||
@@ -1735,10 +1801,37 @@ impl ManagedClientTask {
|
|||||||
Poll::Ready(Err(_)) => SendResult::Failed { accepted },
|
Poll::Ready(Err(_)) => SendResult::Failed { accepted },
|
||||||
};
|
};
|
||||||
self.record_stagger_acceptance(send_result, stagger_spacing, Instant::now());
|
self.record_stagger_acceptance(send_result, stagger_spacing, Instant::now());
|
||||||
let schedule_error = accepted
|
if accepted {
|
||||||
.then(|| client.skip_missed_probe_slots_at(Instant::now()))
|
self.targets[index]
|
||||||
.transpose()
|
.schedule
|
||||||
.err();
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.commit(commit);
|
||||||
|
if let Poll::Ready(Ok(events)) = &mut result {
|
||||||
|
for event in events {
|
||||||
|
if let ClientEvent::EchoSent {
|
||||||
|
scheduled_at: intended,
|
||||||
|
sent_at,
|
||||||
|
timer_error,
|
||||||
|
..
|
||||||
|
} = event
|
||||||
|
{
|
||||||
|
*intended = Some(scheduled_at);
|
||||||
|
*timer_error = Some(instant_abs_diff(sent_at.mono, scheduled_at));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let schedule_error = if accepted {
|
||||||
|
self.targets[index]
|
||||||
|
.schedule
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.skip_missed_slots_at(Instant::now())
|
||||||
|
.err()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
self.targets[index].record_packets_sent(after);
|
self.targets[index].record_packets_sent(after);
|
||||||
match result {
|
match result {
|
||||||
Poll::Pending => {
|
Poll::Pending => {
|
||||||
@@ -1754,7 +1847,7 @@ impl ManagedClientTask {
|
|||||||
let TargetState::Active { client } =
|
let TargetState::Active { client } =
|
||||||
mem::replace(&mut self.targets[index].state, TargetState::Terminal)
|
mem::replace(&mut self.targets[index].state, TargetState::Terminal)
|
||||||
else {
|
else {
|
||||||
unreachable!("probe sender remained active while publishing send events");
|
unreachable!("sender remained active")
|
||||||
};
|
};
|
||||||
self.begin_open_session_failure(
|
self.begin_open_session_failure(
|
||||||
index,
|
index,
|
||||||
@@ -1769,6 +1862,7 @@ impl ManagedClientTask {
|
|||||||
SendResult::Ready { accepted }
|
SendResult::Ready { accepted }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Poll::Ready(Err(error)) => {
|
Poll::Ready(Err(error)) => {
|
||||||
self.targets[index].send_waiting = false;
|
self.targets[index].send_waiting = false;
|
||||||
self.begin_open_session_failure(
|
self.begin_open_session_failure(
|
||||||
@@ -2032,7 +2126,12 @@ impl ManagedClientTask {
|
|||||||
if target.desired && target.retirement.is_none() && !target.send_waiting {
|
if target.desired && target.retirement.is_none() && !target.send_waiting {
|
||||||
send_deadline = send_deadline
|
send_deadline = send_deadline
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.chain(client.next_send_deadline())
|
.chain(
|
||||||
|
target
|
||||||
|
.schedule
|
||||||
|
.as_ref()
|
||||||
|
.and_then(ProbeSchedule::next_send_deadline),
|
||||||
|
)
|
||||||
.min();
|
.min();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2349,6 +2448,7 @@ fn build_task(
|
|||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
runtimes.push(TargetRuntime {
|
runtimes.push(TargetRuntime {
|
||||||
|
schedule: None,
|
||||||
instance: TargetInstance {
|
instance: TargetInstance {
|
||||||
id: target.id.clone(),
|
id: target.id.clone(),
|
||||||
generation,
|
generation,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use std::{
|
|||||||
sync::{Arc, Condvar, Mutex},
|
sync::{Arc, Condvar, Mutex},
|
||||||
task::{Context, Poll, Waker},
|
task::{Context, Poll, Waker},
|
||||||
thread::{self, JoinHandle},
|
thread::{self, JoinHandle},
|
||||||
time::{Duration, Instant, SystemTime},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
use irtt_proto::{
|
use irtt_proto::{
|
||||||
@@ -16,10 +16,7 @@ use irtt_proto::{
|
|||||||
use tokio::runtime::{Builder, Runtime};
|
use tokio::runtime::{Builder, Runtime};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{socket::resolution_call_counts, ClientAuthConfig, NegotiationPolicy, RunMode};
|
||||||
probe::PendingProbe, socket::resolution_call_counts, ClientAuthConfig, ClientTimestamp,
|
|
||||||
NegotiationPolicy, RunMode,
|
|
||||||
};
|
|
||||||
|
|
||||||
const TOKEN: u64 = 0x1234_5678_90ab_cdef;
|
const TOKEN: u64 = 0x1234_5678_90ab_cdef;
|
||||||
|
|
||||||
@@ -337,110 +334,6 @@ fn wait_flag(flag: &Arc<(Mutex<bool>, Condvar)>) {
|
|||||||
assert!(*value && !timeout.timed_out());
|
assert!(*value && !timeout.timed_out());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn client_events(
|
|
||||||
observations: &Mutex<Vec<(ManagedEvent, Arc<ManagedStatus>)>>,
|
|
||||||
) -> Vec<ClientEvent> {
|
|
||||||
observations
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.iter()
|
|
||||||
.filter_map(|(event, _)| match event {
|
|
||||||
ManagedEvent::Client { event, .. } => Some(event.clone()),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn timeout_probe(seq: u32, sent_at: Instant, timeout_at: Instant) -> PendingProbe {
|
|
||||||
PendingProbe {
|
|
||||||
wire_seq: seq,
|
|
||||||
sent_at: ClientTimestamp {
|
|
||||||
mono: sent_at,
|
|
||||||
wall: SystemTime::UNIX_EPOCH,
|
|
||||||
},
|
|
||||||
timeout_at,
|
|
||||||
tx_not_before_wall: SystemTime::UNIX_EPOCH,
|
|
||||||
kernel_tx_timestamp: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_target_for_timeout_test(runtime: &Runtime, task: &mut ManagedClientTask, index: usize) {
|
|
||||||
let TargetState::Pending { client_config } =
|
|
||||||
mem::replace(&mut task.targets[index].state, TargetState::Terminal)
|
|
||||||
else {
|
|
||||||
panic!("timeout test target must start pending");
|
|
||||||
};
|
|
||||||
let client = runtime.block_on(async {
|
|
||||||
let mut client = AsyncClient::connect(client_config).await.unwrap();
|
|
||||||
client.open().await.unwrap();
|
|
||||||
client
|
|
||||||
});
|
|
||||||
task.targets[index].desired = false;
|
|
||||||
task.install_target_state(index, TargetState::Active { client });
|
|
||||||
}
|
|
||||||
|
|
||||||
fn replace_pending_for_timeout_test(
|
|
||||||
task: &mut ManagedClientTask,
|
|
||||||
index: usize,
|
|
||||||
probe: PendingProbe,
|
|
||||||
) {
|
|
||||||
let client = match &mut task.targets[index].state {
|
|
||||||
TargetState::Active { client } | TargetState::Draining { client, .. } => client,
|
|
||||||
_ => panic!("timeout test target must be active or draining"),
|
|
||||||
};
|
|
||||||
client.replace_pending_for_test(probe);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_pending_for_timeout_test(
|
|
||||||
task: &mut ManagedClientTask,
|
|
||||||
index: usize,
|
|
||||||
wire_seq: u32,
|
|
||||||
) -> PendingProbe {
|
|
||||||
let client = match &mut task.targets[index].state {
|
|
||||||
TargetState::Active { client } | TargetState::Draining { client, .. } => client,
|
|
||||||
_ => panic!("timeout test target must be active or draining"),
|
|
||||||
};
|
|
||||||
client
|
|
||||||
.remove_pending_for_test(wire_seq)
|
|
||||||
.expect("timeout test pending probe must exist")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn close_timeout_test_target(runtime: &Runtime, task: &mut ManagedClientTask, index: usize) {
|
|
||||||
let (TargetState::Active { mut client } | TargetState::Draining { mut client, .. }) =
|
|
||||||
mem::replace(&mut task.targets[index].state, TargetState::Terminal)
|
|
||||||
else {
|
|
||||||
panic!("timeout test target did not remain active or draining");
|
|
||||||
};
|
|
||||||
runtime.block_on(client.close()).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_task_once(runtime: &Runtime, task: &mut Pin<Box<ManagedClientTask>>) {
|
|
||||||
runtime.block_on(poll_fn(|cx| {
|
|
||||||
assert!(task.as_mut().poll(cx).is_pending());
|
|
||||||
Poll::Ready(())
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn timeout_losses_for(
|
|
||||||
observations: &Mutex<Vec<(ManagedEvent, Arc<ManagedStatus>)>>,
|
|
||||||
id: &str,
|
|
||||||
) -> usize {
|
|
||||||
observations
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.iter()
|
|
||||||
.filter(|(event, _)| {
|
|
||||||
matches!(
|
|
||||||
event,
|
|
||||||
ManagedEvent::Client {
|
|
||||||
target,
|
|
||||||
event: ClientEvent::EchoLoss { .. },
|
|
||||||
} if target.id.as_ref() == id
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.count()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn probes(records: &[PacketRecord]) -> Vec<Instant> {
|
fn probes(records: &[PacketRecord]) -> Vec<Instant> {
|
||||||
records
|
records
|
||||||
.iter()
|
.iter()
|
||||||
@@ -471,7 +364,12 @@ fn echo_sends(
|
|||||||
sent_at,
|
sent_at,
|
||||||
..
|
..
|
||||||
},
|
},
|
||||||
} => Some((target.clone(), *seq, *scheduled_at, sent_at.mono)),
|
} => Some((
|
||||||
|
target.clone(),
|
||||||
|
*seq,
|
||||||
|
scheduled_at.expect("managed sends carry schedule metadata"),
|
||||||
|
sent_at.mono,
|
||||||
|
)),
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
@@ -1638,83 +1536,6 @@ fn peer_close_during_stop_drain_remains_authoritative() {
|
|||||||
assert!(!has_close(&records));
|
assert!(!has_close(&records));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn stop_during_opening_finishes_in_flight_work() {
|
|
||||||
let open_gate = Arc::new(PacketGate::default());
|
|
||||||
let server = start_server_with_gates(
|
|
||||||
ServerBehavior::Echo,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
Some(Arc::clone(&open_gate)),
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
let mut managed = config(ManagedPacing::Staggered);
|
|
||||||
managed.completion = ManagedCompletionPolicy::ExplicitStop;
|
|
||||||
managed.client.duration = None;
|
|
||||||
managed.client.open_timeouts = vec![Duration::from_secs(1), Duration::from_secs(1)];
|
|
||||||
let (task, handle) =
|
|
||||||
ManagedClient::task(managed, vec![target("success", server.addr)]).unwrap();
|
|
||||||
let runtime = runtime();
|
|
||||||
let mut task = Box::pin(task);
|
|
||||||
drive_task_until(&runtime, &mut task, |task| match &task.targets[0].state {
|
|
||||||
TargetState::Opening { open, .. } => open.has_in_flight_work(),
|
|
||||||
_ => false,
|
|
||||||
});
|
|
||||||
drop(handle.stop());
|
|
||||||
open_gate.release();
|
|
||||||
let outcome = runtime.block_on(task);
|
|
||||||
let records = server.finish();
|
|
||||||
assert_eq!(outcome.end_reason, ManagedEndReason::StopRequested);
|
|
||||||
assert!(matches!(
|
|
||||||
outcome.recent_target_outcomes[0].end_reason,
|
|
||||||
ManagedTargetEndReason::Stopped
|
|
||||||
));
|
|
||||||
assert_eq!(
|
|
||||||
records
|
|
||||||
.iter()
|
|
||||||
.filter(|record| matches!(record.kind, PacketKind::Open))
|
|
||||||
.count(),
|
|
||||||
1
|
|
||||||
);
|
|
||||||
assert!(probes(&records).is_empty() && has_close(&records));
|
|
||||||
let key = b"managed-retained-open-cleanup".to_vec();
|
|
||||||
let cleanup_server = start_server(ServerBehavior::Echo, Some(key.clone()));
|
|
||||||
let mut configured = target("cleanup", cleanup_server.addr);
|
|
||||||
configured.auth = Some(ClientAuthConfig {
|
|
||||||
hmac_key: Some(key),
|
|
||||||
});
|
|
||||||
let mut managed = config(ManagedPacing::Staggered);
|
|
||||||
managed.completion = ManagedCompletionPolicy::ExplicitStop;
|
|
||||||
managed.client.duration = None;
|
|
||||||
let (task, handle) = ManagedClient::task(managed, vec![configured]).unwrap();
|
|
||||||
let mut task = Box::pin(task);
|
|
||||||
drive_task_until(&runtime, &mut task, |task| {
|
|
||||||
matches!(task.targets[0].state, TargetState::Opening { .. })
|
|
||||||
});
|
|
||||||
let TargetState::Opening { client, .. } = &task.as_ref().get_ref().targets[0].state else {
|
|
||||||
unreachable!()
|
|
||||||
};
|
|
||||||
client.retain_open_cleanup_once();
|
|
||||||
drive_task_until(&runtime, &mut task, |task| match &task.targets[0].state {
|
|
||||||
TargetState::Opening { open, .. } => open.has_retained_cleanup(),
|
|
||||||
_ => false,
|
|
||||||
});
|
|
||||||
drop(handle.stop());
|
|
||||||
let outcome = runtime.block_on(task);
|
|
||||||
let records = cleanup_server.finish();
|
|
||||||
let target = &outcome.recent_target_outcomes[0];
|
|
||||||
assert!(matches!(target.end_reason, ManagedTargetEndReason::Stopped));
|
|
||||||
assert_eq!(outcome.failed_target_outcomes, 0);
|
|
||||||
assert!(matches!(
|
|
||||||
target.cleanup_failure,
|
|
||||||
Some(ManagedTargetFailure {
|
|
||||||
phase: ManagedTargetFailurePhase::Opening,
|
|
||||||
..
|
|
||||||
})
|
|
||||||
));
|
|
||||||
assert!(probes(&records).is_empty() && has_close(&records));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn static_multi_target_completion() {
|
fn static_multi_target_completion() {
|
||||||
let first = start_server(ServerBehavior::Echo, None);
|
let first = start_server(ServerBehavior::Echo, None);
|
||||||
@@ -1983,768 +1804,6 @@ fn timeout_discovery_inspects_at_most_one_budget_of_targets() {
|
|||||||
assert_eq!(task.timeout_inspections, TIMEOUT_WORK_BUDGET);
|
assert_eq!(task.timeout_inspections, TIMEOUT_WORK_BUDGET);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn sparse_timeout_backlog_persists_across_polls() {
|
|
||||||
let server = start_server(ServerBehavior::Echo, None);
|
|
||||||
let target_count = (2 * TIMEOUT_WORK_BUDGET) + 1;
|
|
||||||
let mut managed = config(ManagedPacing::Burst);
|
|
||||||
managed.completion = ManagedCompletionPolicy::ExplicitStop;
|
|
||||||
managed.client.duration = None;
|
|
||||||
managed.max_live_target_generations = target_count;
|
|
||||||
let targets = std::iter::once(target("sparse", server.addr))
|
|
||||||
.chain(
|
|
||||||
(1..target_count)
|
|
||||||
.map(|index| ManagedTargetConfig::new(format!("target-{index}"), "127.0.0.1:9")),
|
|
||||||
)
|
|
||||||
.collect();
|
|
||||||
let (mut task, _) = ManagedClient::task(managed, targets).unwrap();
|
|
||||||
for target in &mut task.targets[1..] {
|
|
||||||
target.state = TargetState::Terminal;
|
|
||||||
}
|
|
||||||
let observations = Arc::new(Mutex::new(Vec::new()));
|
|
||||||
task.event_observations = Some(Arc::clone(&observations));
|
|
||||||
let runtime = runtime();
|
|
||||||
open_target_for_timeout_test(&runtime, &mut task, 0);
|
|
||||||
let now = Instant::now();
|
|
||||||
for seq in 0..u32::try_from((2 * TIMEOUT_WORK_BUDGET) + 64).unwrap() {
|
|
||||||
replace_pending_for_timeout_test(
|
|
||||||
&mut task,
|
|
||||||
0,
|
|
||||||
timeout_probe(
|
|
||||||
seq,
|
|
||||||
now - Duration::from_secs(2),
|
|
||||||
now - Duration::from_secs(1),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert!(task.poll_timeout_pass(now));
|
|
||||||
assert_eq!(
|
|
||||||
timeout_losses_for(&observations, "sparse"),
|
|
||||||
TIMEOUT_WORK_BUDGET
|
|
||||||
);
|
|
||||||
assert_eq!(task.timeout_backlog.len(), 1);
|
|
||||||
|
|
||||||
assert!(task.poll_timeout_pass(now));
|
|
||||||
assert_eq!(
|
|
||||||
timeout_losses_for(&observations, "sparse"),
|
|
||||||
2 * TIMEOUT_WORK_BUDGET
|
|
||||||
);
|
|
||||||
assert_eq!(task.timeout_backlog.len(), 1);
|
|
||||||
close_timeout_test_target(&runtime, &mut task, 0);
|
|
||||||
drop(task);
|
|
||||||
server.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn timeout_backlog_does_not_starve_discovery() {
|
|
||||||
let first = start_server(ServerBehavior::Echo, None);
|
|
||||||
let second = start_server(ServerBehavior::Echo, None);
|
|
||||||
let target_count = (2 * TIMEOUT_WORK_BUDGET) + 1;
|
|
||||||
let late_index = target_count - 1;
|
|
||||||
let mut managed = config(ManagedPacing::Burst);
|
|
||||||
managed.completion = ManagedCompletionPolicy::ExplicitStop;
|
|
||||||
managed.client.duration = None;
|
|
||||||
managed.max_live_target_generations = target_count;
|
|
||||||
let targets = (0..target_count)
|
|
||||||
.map(|index| match index {
|
|
||||||
0 => target("backlog", first.addr),
|
|
||||||
index if index == late_index => target("late", second.addr),
|
|
||||||
_ => ManagedTargetConfig::new(format!("target-{index}"), "127.0.0.1:9"),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let (mut task, _) = ManagedClient::task(managed, targets).unwrap();
|
|
||||||
for target in &mut task.targets[1..late_index] {
|
|
||||||
target.state = TargetState::Terminal;
|
|
||||||
}
|
|
||||||
let observations = Arc::new(Mutex::new(Vec::new()));
|
|
||||||
task.event_observations = Some(Arc::clone(&observations));
|
|
||||||
let runtime = runtime();
|
|
||||||
open_target_for_timeout_test(&runtime, &mut task, 0);
|
|
||||||
open_target_for_timeout_test(&runtime, &mut task, late_index);
|
|
||||||
let now = Instant::now();
|
|
||||||
for seq in 0..u32::try_from(3 * TIMEOUT_WORK_BUDGET).unwrap() {
|
|
||||||
replace_pending_for_timeout_test(
|
|
||||||
&mut task,
|
|
||||||
0,
|
|
||||||
timeout_probe(
|
|
||||||
seq,
|
|
||||||
now - Duration::from_secs(2),
|
|
||||||
now - Duration::from_secs(1),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
replace_pending_for_timeout_test(
|
|
||||||
&mut task,
|
|
||||||
late_index,
|
|
||||||
timeout_probe(
|
|
||||||
0,
|
|
||||||
now - Duration::from_secs(2),
|
|
||||||
now - Duration::from_secs(1),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
task.timeout_backlog.push_back(TimeoutBacklogEntry {
|
|
||||||
index: 0,
|
|
||||||
generation: task.targets[0].instance.generation,
|
|
||||||
});
|
|
||||||
task.timeout_cursor = 1;
|
|
||||||
|
|
||||||
assert!(task.poll_timeout_pass(now));
|
|
||||||
assert_eq!(timeout_losses_for(&observations, "late"), 0);
|
|
||||||
|
|
||||||
assert!(task.poll_timeout_pass(now));
|
|
||||||
assert_eq!(timeout_losses_for(&observations, "late"), 1);
|
|
||||||
assert_eq!(task.timeout_backlog.len(), 1);
|
|
||||||
close_timeout_test_target(&runtime, &mut task, 0);
|
|
||||||
close_timeout_test_target(&runtime, &mut task, late_index);
|
|
||||||
drop(task);
|
|
||||||
first.finish();
|
|
||||||
second.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn stale_timeout_backlog_entry_is_dropped_after_pruning() {
|
|
||||||
let server = start_server(ServerBehavior::Echo, None);
|
|
||||||
let target_count = TIMEOUT_WORK_BUDGET + 2;
|
|
||||||
let mut managed = config(ManagedPacing::Burst);
|
|
||||||
managed.completion = ManagedCompletionPolicy::ExplicitStop;
|
|
||||||
managed.client.duration = None;
|
|
||||||
managed.max_live_target_generations = target_count;
|
|
||||||
let targets = vec![
|
|
||||||
ManagedTargetConfig::new("retired", "127.0.0.1:9"),
|
|
||||||
target("survivor", server.addr),
|
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
.chain(
|
|
||||||
(2..target_count)
|
|
||||||
.map(|index| ManagedTargetConfig::new(format!("target-{index}"), "127.0.0.1:9")),
|
|
||||||
)
|
|
||||||
.collect();
|
|
||||||
let (mut task, _) = ManagedClient::task(managed, targets).unwrap();
|
|
||||||
let observations = Arc::new(Mutex::new(Vec::new()));
|
|
||||||
task.event_observations = Some(Arc::clone(&observations));
|
|
||||||
let runtime = runtime();
|
|
||||||
open_target_for_timeout_test(&runtime, &mut task, 1);
|
|
||||||
let now = Instant::now();
|
|
||||||
replace_pending_for_timeout_test(
|
|
||||||
&mut task,
|
|
||||||
1,
|
|
||||||
timeout_probe(
|
|
||||||
0,
|
|
||||||
now - Duration::from_secs(2),
|
|
||||||
now - Duration::from_secs(1),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
task.timeout_backlog.push_back(TimeoutBacklogEntry {
|
|
||||||
index: 0,
|
|
||||||
generation: task.targets[0].instance.generation,
|
|
||||||
});
|
|
||||||
task.targets[0].desired = false;
|
|
||||||
task.targets[0].state = TargetState::Terminal;
|
|
||||||
task.prune_undesired_terminal();
|
|
||||||
task.timeout_cursor = 1;
|
|
||||||
|
|
||||||
assert!(!task.poll_timeout_pass(now));
|
|
||||||
assert!(task.timeout_backlog.is_empty());
|
|
||||||
assert_eq!(timeout_losses_for(&observations, "survivor"), 0);
|
|
||||||
|
|
||||||
task.timeout_cursor = 0;
|
|
||||||
assert!(!task.poll_timeout_pass(now));
|
|
||||||
assert_eq!(timeout_losses_for(&observations, "survivor"), 1);
|
|
||||||
close_timeout_test_target(&runtime, &mut task, 0);
|
|
||||||
drop(task);
|
|
||||||
server.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn immediate_timeout_backlog_skips_global_deadline_scan() {
|
|
||||||
let server = start_server(ServerBehavior::Echo, None);
|
|
||||||
let mut managed = config(ManagedPacing::Burst);
|
|
||||||
managed.completion = ManagedCompletionPolicy::ExplicitStop;
|
|
||||||
managed.client.duration = None;
|
|
||||||
let (mut task, _) = ManagedClient::task(managed, vec![target("one", server.addr)]).unwrap();
|
|
||||||
let runtime = runtime();
|
|
||||||
open_target_for_timeout_test(&runtime, &mut task, 0);
|
|
||||||
let now = Instant::now();
|
|
||||||
for seq in 0..u32::try_from(TIMEOUT_WORK_BUDGET + 1).unwrap() {
|
|
||||||
replace_pending_for_timeout_test(
|
|
||||||
&mut task,
|
|
||||||
0,
|
|
||||||
timeout_probe(
|
|
||||||
seq,
|
|
||||||
now - Duration::from_secs(2),
|
|
||||||
now - Duration::from_secs(1),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
task.timeout_backlog.push_back(TimeoutBacklogEntry {
|
|
||||||
index: 0,
|
|
||||||
generation: task.targets[0].instance.generation,
|
|
||||||
});
|
|
||||||
task.deadline_inspections = 0;
|
|
||||||
|
|
||||||
let mut task = Box::pin(task);
|
|
||||||
poll_task_once(&runtime, &mut task);
|
|
||||||
assert_eq!(task.deadline_inspections, 0);
|
|
||||||
assert_eq!(task.timeout_backlog.len(), 1);
|
|
||||||
|
|
||||||
assert!(!task.poll_timeout_pass(now));
|
|
||||||
assert!(task.timeout_backlog.is_empty());
|
|
||||||
task.deadline_inspections = 0;
|
|
||||||
poll_task_once(&runtime, &mut task);
|
|
||||||
assert_eq!(task.deadline_inspections, 1);
|
|
||||||
close_timeout_test_target(&runtime, &mut task, 0);
|
|
||||||
drop(task);
|
|
||||||
server.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn timeout_discovery_reaches_later_target_after_budget_slice() {
|
|
||||||
let server = start_server(ServerBehavior::Echo, None);
|
|
||||||
let target_count = TIMEOUT_WORK_BUDGET + 1;
|
|
||||||
let late_index = target_count - 1;
|
|
||||||
let mut managed = config(ManagedPacing::Burst);
|
|
||||||
managed.completion = ManagedCompletionPolicy::ExplicitStop;
|
|
||||||
managed.client.duration = None;
|
|
||||||
managed.max_live_target_generations = target_count;
|
|
||||||
let targets = (0..target_count)
|
|
||||||
.map(|index| {
|
|
||||||
if index == late_index {
|
|
||||||
target("late", server.addr)
|
|
||||||
} else {
|
|
||||||
ManagedTargetConfig::new(format!("target-{index}"), "127.0.0.1:9")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let (mut task, _) = ManagedClient::task(managed, targets).unwrap();
|
|
||||||
for target in &mut task.targets[..late_index] {
|
|
||||||
target.state = TargetState::Terminal;
|
|
||||||
}
|
|
||||||
let observations = Arc::new(Mutex::new(Vec::new()));
|
|
||||||
task.event_observations = Some(Arc::clone(&observations));
|
|
||||||
let runtime = runtime();
|
|
||||||
open_target_for_timeout_test(&runtime, &mut task, late_index);
|
|
||||||
let now = Instant::now();
|
|
||||||
replace_pending_for_timeout_test(
|
|
||||||
&mut task,
|
|
||||||
late_index,
|
|
||||||
timeout_probe(0, now - Duration::from_secs(1), now),
|
|
||||||
);
|
|
||||||
|
|
||||||
task.timeout_inspections = 0;
|
|
||||||
assert!(!task.poll_timeout_pass(now));
|
|
||||||
assert_eq!(task.timeout_inspections, TIMEOUT_WORK_BUDGET);
|
|
||||||
assert_eq!(timeout_losses_for(&observations, "late"), 0);
|
|
||||||
|
|
||||||
task.timeout_inspections = 0;
|
|
||||||
assert!(!task.poll_timeout_pass(now));
|
|
||||||
assert_eq!(task.timeout_inspections, TIMEOUT_WORK_BUDGET);
|
|
||||||
assert_eq!(timeout_losses_for(&observations, "late"), 1);
|
|
||||||
|
|
||||||
let TargetState::Active { mut client } =
|
|
||||||
mem::replace(&mut task.targets[late_index].state, TargetState::Terminal)
|
|
||||||
else {
|
|
||||||
panic!("later timeout target did not remain active");
|
|
||||||
};
|
|
||||||
runtime.block_on(client.close()).unwrap();
|
|
||||||
drop(task);
|
|
||||||
server.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn timeout_work_is_bounded_per_task_poll_and_eventually_drains() {
|
|
||||||
let server = start_server(ServerBehavior::Echo, None);
|
|
||||||
let mut managed = config(ManagedPacing::Staggered);
|
|
||||||
managed.completion = ManagedCompletionPolicy::ExplicitStop;
|
|
||||||
managed.client.duration = None;
|
|
||||||
let (mut task, handle) =
|
|
||||||
ManagedClient::task(managed, vec![target("one", server.addr)]).unwrap();
|
|
||||||
let observations = Arc::new(Mutex::new(Vec::new()));
|
|
||||||
task.event_observations = Some(Arc::clone(&observations));
|
|
||||||
let runtime = runtime();
|
|
||||||
open_target_for_timeout_test(&runtime, &mut task, 0);
|
|
||||||
let now = Instant::now();
|
|
||||||
for seq in 0..u32::try_from(TIMEOUT_WORK_BUDGET + 1).unwrap() {
|
|
||||||
replace_pending_for_timeout_test(
|
|
||||||
&mut task,
|
|
||||||
0,
|
|
||||||
timeout_probe(
|
|
||||||
seq,
|
|
||||||
now - Duration::from_secs(2),
|
|
||||||
now - Duration::from_secs(1),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut task = Box::pin(task);
|
|
||||||
poll_task_once(&runtime, &mut task);
|
|
||||||
assert_eq!(
|
|
||||||
timeout_losses_for(&observations, "one"),
|
|
||||||
TIMEOUT_WORK_BUDGET
|
|
||||||
);
|
|
||||||
assert!(matches!(
|
|
||||||
&task.targets[0].state,
|
|
||||||
TargetState::Active { client }
|
|
||||||
if client.next_probe_timeout_deadline().is_some()
|
|
||||||
));
|
|
||||||
|
|
||||||
drive_task_until(&runtime, &mut task, |task| {
|
|
||||||
matches!(
|
|
||||||
&task.targets[0].state,
|
|
||||||
TargetState::Active { client }
|
|
||||||
if client.next_probe_timeout_deadline().is_none()
|
|
||||||
)
|
|
||||||
});
|
|
||||||
assert_eq!(
|
|
||||||
timeout_losses_for(&observations, "one"),
|
|
||||||
TIMEOUT_WORK_BUDGET + 1
|
|
||||||
);
|
|
||||||
|
|
||||||
drop(handle.stop());
|
|
||||||
runtime.block_on(task);
|
|
||||||
server.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn timeout_work_round_robins_simultaneous_target_backlogs() {
|
|
||||||
let first = start_server(ServerBehavior::Echo, None);
|
|
||||||
let second = start_server(ServerBehavior::Echo, None);
|
|
||||||
let mut managed = config(ManagedPacing::Staggered);
|
|
||||||
managed.completion = ManagedCompletionPolicy::ExplicitStop;
|
|
||||||
managed.client.duration = None;
|
|
||||||
let (mut task, handle) = ManagedClient::task(
|
|
||||||
managed,
|
|
||||||
vec![target("first", first.addr), target("second", second.addr)],
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let observations = Arc::new(Mutex::new(Vec::new()));
|
|
||||||
task.event_observations = Some(Arc::clone(&observations));
|
|
||||||
let runtime = runtime();
|
|
||||||
open_target_for_timeout_test(&runtime, &mut task, 0);
|
|
||||||
open_target_for_timeout_test(&runtime, &mut task, 1);
|
|
||||||
let now = Instant::now();
|
|
||||||
let per_target = TIMEOUT_WORK_BUDGET + 16;
|
|
||||||
for index in 0..2 {
|
|
||||||
for seq in 0..u32::try_from(per_target).unwrap() {
|
|
||||||
replace_pending_for_timeout_test(
|
|
||||||
&mut task,
|
|
||||||
index,
|
|
||||||
timeout_probe(
|
|
||||||
seq,
|
|
||||||
now - Duration::from_secs(2),
|
|
||||||
now - Duration::from_secs(1),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut task = Box::pin(task);
|
|
||||||
poll_task_once(&runtime, &mut task);
|
|
||||||
assert_eq!(
|
|
||||||
timeout_losses_for(&observations, "first"),
|
|
||||||
TIMEOUT_WORK_BUDGET / 2
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
timeout_losses_for(&observations, "second"),
|
|
||||||
TIMEOUT_WORK_BUDGET / 2
|
|
||||||
);
|
|
||||||
assert!(matches!(
|
|
||||||
&task.targets[0].state,
|
|
||||||
TargetState::Active { client }
|
|
||||||
if client.next_probe_timeout_deadline().is_some()
|
|
||||||
));
|
|
||||||
assert!(matches!(
|
|
||||||
&task.targets[1].state,
|
|
||||||
TargetState::Active { client }
|
|
||||||
if client.next_probe_timeout_deadline().is_some()
|
|
||||||
));
|
|
||||||
|
|
||||||
drive_task_until(&runtime, &mut task, |task| {
|
|
||||||
task.targets.iter().all(|target| {
|
|
||||||
matches!(
|
|
||||||
&target.state,
|
|
||||||
TargetState::Active { client }
|
|
||||||
if client.next_probe_timeout_deadline().is_none()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
assert_eq!(timeout_losses_for(&observations, "first"), per_target);
|
|
||||||
assert_eq!(timeout_losses_for(&observations, "second"), per_target);
|
|
||||||
|
|
||||||
drop(handle.stop());
|
|
||||||
runtime.block_on(task);
|
|
||||||
first.finish();
|
|
||||||
second.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn budget_deferred_timeout_blocks_send_until_timeout_processing() {
|
|
||||||
let reply_gate = Arc::new(PacketGate::default());
|
|
||||||
let server = start_server_with_gates(
|
|
||||||
ServerBehavior::Echo,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
Some(Arc::clone(&reply_gate)),
|
|
||||||
);
|
|
||||||
let target_count = TIMEOUT_WORK_BUDGET + 1;
|
|
||||||
let late_index = target_count - 1;
|
|
||||||
let mut managed = config(ManagedPacing::Burst);
|
|
||||||
managed.completion = ManagedCompletionPolicy::ExplicitStop;
|
|
||||||
managed.client.duration = None;
|
|
||||||
managed.client.interval = Duration::from_nanos(1);
|
|
||||||
managed.client.probe_timeout = Duration::from_secs(1);
|
|
||||||
managed.client.max_pending_probes = 1;
|
|
||||||
managed.max_live_target_generations = target_count;
|
|
||||||
let targets = (0..target_count)
|
|
||||||
.map(|index| {
|
|
||||||
if index == late_index {
|
|
||||||
target("late", server.addr)
|
|
||||||
} else {
|
|
||||||
ManagedTargetConfig::new(format!("target-{index}"), "127.0.0.1:9")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let (mut task, _) = ManagedClient::task(managed, targets).unwrap();
|
|
||||||
for target in &mut task.targets[..late_index] {
|
|
||||||
target.state = TargetState::Terminal;
|
|
||||||
}
|
|
||||||
let observations = Arc::new(Mutex::new(Vec::new()));
|
|
||||||
task.event_observations = Some(Arc::clone(&observations));
|
|
||||||
let runtime = runtime();
|
|
||||||
open_target_for_timeout_test(&runtime, &mut task, late_index);
|
|
||||||
task.targets[late_index].desired = true;
|
|
||||||
let sent = match &mut task.targets[late_index].state {
|
|
||||||
TargetState::Active { client } => runtime.block_on(client.send_probe()).unwrap(),
|
|
||||||
_ => panic!("deferred-send target did not open"),
|
|
||||||
};
|
|
||||||
assert!(matches!(
|
|
||||||
sent.as_slice(),
|
|
||||||
[ClientEvent::EchoSent { seq: 0, .. }]
|
|
||||||
));
|
|
||||||
wait_flag(&server.probe_seen);
|
|
||||||
|
|
||||||
let overdue = Instant::now();
|
|
||||||
replace_pending_for_timeout_test(
|
|
||||||
&mut task,
|
|
||||||
late_index,
|
|
||||||
timeout_probe(0, overdue - Duration::from_secs(1), overdue),
|
|
||||||
);
|
|
||||||
task.state = DriverState::Running;
|
|
||||||
task.lifecycle = ManagedLifecycle::Running;
|
|
||||||
task.timeout_cursor = 0;
|
|
||||||
task.send_cursor = late_index;
|
|
||||||
|
|
||||||
let mut task = Box::pin(task);
|
|
||||||
poll_task_once(&runtime, &mut task);
|
|
||||||
assert_eq!(task.timeout_inspections, TIMEOUT_WORK_BUDGET);
|
|
||||||
assert_eq!(timeout_losses_for(&observations, "late"), 0);
|
|
||||||
assert!(matches!(
|
|
||||||
&task.targets[late_index].state,
|
|
||||||
TargetState::Active { client }
|
|
||||||
if client.packets_sent() == 1
|
|
||||||
&& client.next_probe_timeout_deadline() == Some(overdue)
|
|
||||||
));
|
|
||||||
assert!(!task.targets[late_index].send_waiting);
|
|
||||||
|
|
||||||
poll_task_once(&runtime, &mut task);
|
|
||||||
assert_eq!(timeout_losses_for(&observations, "late"), 1);
|
|
||||||
assert!(matches!(
|
|
||||||
&task.targets[late_index].state,
|
|
||||||
TargetState::Active { client }
|
|
||||||
if client.next_probe_timeout_deadline().is_none()
|
|
||||||
));
|
|
||||||
|
|
||||||
poll_task_once(&runtime, &mut task);
|
|
||||||
assert!(matches!(
|
|
||||||
&task.targets[late_index].state,
|
|
||||||
TargetState::Active { client } if client.packets_sent() == 2
|
|
||||||
));
|
|
||||||
|
|
||||||
reply_gate.release();
|
|
||||||
let TargetState::Active { mut client } =
|
|
||||||
mem::replace(&mut task.targets[late_index].state, TargetState::Terminal)
|
|
||||||
else {
|
|
||||||
panic!("deferred-send target did not remain active");
|
|
||||||
};
|
|
||||||
runtime.block_on(client.close()).unwrap();
|
|
||||||
drop(task);
|
|
||||||
server.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn partial_timeout_batch_blocks_active_receive_until_loss_commits() {
|
|
||||||
let reply_gate = Arc::new(PacketGate::default());
|
|
||||||
let server = start_server_with_gates(
|
|
||||||
ServerBehavior::Echo,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
Some(Arc::clone(&reply_gate)),
|
|
||||||
);
|
|
||||||
let mut managed = config(ManagedPacing::Staggered);
|
|
||||||
managed.completion = ManagedCompletionPolicy::ExplicitStop;
|
|
||||||
managed.client.duration = None;
|
|
||||||
let (mut task, handle) =
|
|
||||||
ManagedClient::task(managed, vec![target("one", server.addr)]).unwrap();
|
|
||||||
let observations = Arc::new(Mutex::new(Vec::new()));
|
|
||||||
task.event_observations = Some(Arc::clone(&observations));
|
|
||||||
let runtime = runtime();
|
|
||||||
open_target_for_timeout_test(&runtime, &mut task, 0);
|
|
||||||
let sent = match &mut task.targets[0].state {
|
|
||||||
TargetState::Active { client } => runtime.block_on(client.send_probe()).unwrap(),
|
|
||||||
_ => panic!("timeout test target did not open"),
|
|
||||||
};
|
|
||||||
assert!(matches!(
|
|
||||||
sent.as_slice(),
|
|
||||||
[ClientEvent::EchoSent { seq: 0, .. }]
|
|
||||||
));
|
|
||||||
wait_flag(&server.probe_seen);
|
|
||||||
|
|
||||||
let now = Instant::now();
|
|
||||||
let _ = remove_pending_for_timeout_test(&mut task, 0, 0);
|
|
||||||
for seq in 1..=u32::try_from(TIMEOUT_WORK_BUDGET).unwrap() {
|
|
||||||
replace_pending_for_timeout_test(
|
|
||||||
&mut task,
|
|
||||||
0,
|
|
||||||
timeout_probe(
|
|
||||||
seq,
|
|
||||||
now - Duration::from_secs(2),
|
|
||||||
now - Duration::from_secs(1),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
replace_pending_for_timeout_test(
|
|
||||||
&mut task,
|
|
||||||
0,
|
|
||||||
timeout_probe(0, now - Duration::from_secs(1), now),
|
|
||||||
);
|
|
||||||
reply_gate.release();
|
|
||||||
wait_flag(&server.reply_sent);
|
|
||||||
|
|
||||||
let mut task = Box::pin(task);
|
|
||||||
poll_task_once(&runtime, &mut task);
|
|
||||||
assert_eq!(
|
|
||||||
timeout_losses_for(&observations, "one"),
|
|
||||||
TIMEOUT_WORK_BUDGET
|
|
||||||
);
|
|
||||||
assert!(!client_events(&observations).iter().any(|event| {
|
|
||||||
matches!(
|
|
||||||
event,
|
|
||||||
ClientEvent::EchoLoss { seq: 0, .. }
|
|
||||||
| ClientEvent::LateReply { seq: 0, .. }
|
|
||||||
| ClientEvent::EchoReply { seq: 0, .. }
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
|
|
||||||
drive_task_until(&runtime, &mut task, |_| {
|
|
||||||
client_events(&observations)
|
|
||||||
.iter()
|
|
||||||
.any(|event| matches!(event, ClientEvent::LateReply { seq: 0, .. }))
|
|
||||||
});
|
|
||||||
let classified = client_events(&observations)
|
|
||||||
.into_iter()
|
|
||||||
.filter(|event| {
|
|
||||||
matches!(
|
|
||||||
event,
|
|
||||||
ClientEvent::EchoLoss { seq: 0, .. }
|
|
||||||
| ClientEvent::LateReply { seq: 0, .. }
|
|
||||||
| ClientEvent::EchoReply { seq: 0, .. }
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
assert!(matches!(
|
|
||||||
classified.as_slice(),
|
|
||||||
[
|
|
||||||
ClientEvent::EchoLoss { seq: 0, .. },
|
|
||||||
ClientEvent::LateReply { seq: 0, .. }
|
|
||||||
]
|
|
||||||
));
|
|
||||||
|
|
||||||
drop(handle.stop());
|
|
||||||
runtime.block_on(task);
|
|
||||||
server.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn partial_timeout_batch_blocks_draining_receive_until_loss_commits() {
|
|
||||||
let reply_gate = Arc::new(PacketGate::default());
|
|
||||||
let server = start_server_with_gates(
|
|
||||||
ServerBehavior::Echo,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
Some(Arc::clone(&reply_gate)),
|
|
||||||
);
|
|
||||||
let mut managed = config(ManagedPacing::Staggered);
|
|
||||||
managed.completion = ManagedCompletionPolicy::ExplicitStop;
|
|
||||||
managed.client.duration = None;
|
|
||||||
let (mut task, handle) =
|
|
||||||
ManagedClient::task(managed, vec![target("one", server.addr)]).unwrap();
|
|
||||||
let observations = Arc::new(Mutex::new(Vec::new()));
|
|
||||||
task.event_observations = Some(Arc::clone(&observations));
|
|
||||||
let runtime = runtime();
|
|
||||||
open_target_for_timeout_test(&runtime, &mut task, 0);
|
|
||||||
let sent = match &mut task.targets[0].state {
|
|
||||||
TargetState::Active { client } => runtime.block_on(client.send_probe()).unwrap(),
|
|
||||||
_ => panic!("timeout test target did not open"),
|
|
||||||
};
|
|
||||||
assert!(matches!(
|
|
||||||
sent.as_slice(),
|
|
||||||
[ClientEvent::EchoSent { seq: 0, .. }]
|
|
||||||
));
|
|
||||||
wait_flag(&server.probe_seen);
|
|
||||||
|
|
||||||
let now = Instant::now();
|
|
||||||
let _ = remove_pending_for_timeout_test(&mut task, 0, 0);
|
|
||||||
for seq in 1..=u32::try_from(TIMEOUT_WORK_BUDGET).unwrap() {
|
|
||||||
replace_pending_for_timeout_test(
|
|
||||||
&mut task,
|
|
||||||
0,
|
|
||||||
timeout_probe(
|
|
||||||
seq,
|
|
||||||
now - Duration::from_secs(2),
|
|
||||||
now - Duration::from_secs(1),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
replace_pending_for_timeout_test(
|
|
||||||
&mut task,
|
|
||||||
0,
|
|
||||||
timeout_probe(0, now - Duration::from_secs(1), now),
|
|
||||||
);
|
|
||||||
let TargetState::Active { client } =
|
|
||||||
mem::replace(&mut task.targets[0].state, TargetState::Terminal)
|
|
||||||
else {
|
|
||||||
panic!("timeout test target did not remain active");
|
|
||||||
};
|
|
||||||
assert!(task.begin_drain(0, client, ManagedTargetEndReason::Stopped, now));
|
|
||||||
reply_gate.release();
|
|
||||||
wait_flag(&server.reply_sent);
|
|
||||||
|
|
||||||
let mut task = Box::pin(task);
|
|
||||||
poll_task_once(&runtime, &mut task);
|
|
||||||
assert_eq!(
|
|
||||||
timeout_losses_for(&observations, "one"),
|
|
||||||
TIMEOUT_WORK_BUDGET
|
|
||||||
);
|
|
||||||
assert!(matches!(
|
|
||||||
&task.targets[0].state,
|
|
||||||
TargetState::Draining {
|
|
||||||
primary_end: ManagedTargetEndReason::Stopped,
|
|
||||||
cleanup_failure: None,
|
|
||||||
..
|
|
||||||
}
|
|
||||||
));
|
|
||||||
assert!(!client_events(&observations).iter().any(|event| {
|
|
||||||
matches!(
|
|
||||||
event,
|
|
||||||
ClientEvent::EchoLoss { seq: 0, .. }
|
|
||||||
| ClientEvent::LateReply { seq: 0, .. }
|
|
||||||
| ClientEvent::EchoReply { seq: 0, .. }
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
|
|
||||||
drive_task_until(&runtime, &mut task, |_| {
|
|
||||||
client_events(&observations)
|
|
||||||
.iter()
|
|
||||||
.any(|event| matches!(event, ClientEvent::LateReply { seq: 0, .. }))
|
|
||||||
});
|
|
||||||
let classified = client_events(&observations)
|
|
||||||
.into_iter()
|
|
||||||
.filter(|event| {
|
|
||||||
matches!(
|
|
||||||
event,
|
|
||||||
ClientEvent::EchoLoss { seq: 0, .. }
|
|
||||||
| ClientEvent::LateReply { seq: 0, .. }
|
|
||||||
| ClientEvent::EchoReply { seq: 0, .. }
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
assert!(matches!(
|
|
||||||
classified.as_slice(),
|
|
||||||
[
|
|
||||||
ClientEvent::EchoLoss { seq: 0, .. },
|
|
||||||
ClientEvent::LateReply { seq: 0, .. }
|
|
||||||
]
|
|
||||||
));
|
|
||||||
|
|
||||||
drop(handle.stop());
|
|
||||||
let outcome = runtime.block_on(task);
|
|
||||||
let target = &outcome.recent_target_outcomes[0];
|
|
||||||
assert!(matches!(target.end_reason, ManagedTargetEndReason::Stopped));
|
|
||||||
assert!(target.cleanup_failure.is_none());
|
|
||||||
server.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn overdue_queued_reply_is_lost_then_late() {
|
|
||||||
let reply_gate = Arc::new(PacketGate::default());
|
|
||||||
let server = start_server_with_gates(
|
|
||||||
ServerBehavior::Echo,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
Some(Arc::clone(&reply_gate)),
|
|
||||||
);
|
|
||||||
let mut managed = config(ManagedPacing::Staggered);
|
|
||||||
managed.completion = ManagedCompletionPolicy::ExplicitStop;
|
|
||||||
managed.client.duration = None;
|
|
||||||
managed.client.interval = Duration::from_secs(1);
|
|
||||||
managed.client.probe_timeout = Duration::from_millis(30);
|
|
||||||
let (mut task, handle) =
|
|
||||||
ManagedClient::task(managed, vec![target("one", server.addr)]).unwrap();
|
|
||||||
let observations = Arc::new(Mutex::new(Vec::new()));
|
|
||||||
task.event_observations = Some(Arc::clone(&observations));
|
|
||||||
let runtime = runtime();
|
|
||||||
let mut task = Box::pin(task);
|
|
||||||
drive_task_until(&runtime, &mut task, |task| {
|
|
||||||
task.targets[0].counters.packets_sent == 1
|
|
||||||
});
|
|
||||||
let timeout = match &task.targets[0].state {
|
|
||||||
TargetState::Active { client } => client.next_probe_timeout_deadline().unwrap(),
|
|
||||||
_ => panic!("probe sender did not remain active"),
|
|
||||||
};
|
|
||||||
thread::sleep(timeout.saturating_duration_since(Instant::now()));
|
|
||||||
while Instant::now() <= timeout {
|
|
||||||
thread::yield_now();
|
|
||||||
}
|
|
||||||
reply_gate.release();
|
|
||||||
wait_flag(&server.reply_sent);
|
|
||||||
runtime.block_on(poll_fn(|cx| {
|
|
||||||
let ready = match &task.as_ref().get_ref().targets[0].state {
|
|
||||||
TargetState::Active { client } => client.poll_recv_ready_for_test(cx),
|
|
||||||
_ => panic!("probe sender did not remain active"),
|
|
||||||
};
|
|
||||||
ready.map(|result| result.unwrap())
|
|
||||||
}));
|
|
||||||
runtime.block_on(poll_fn(|cx| {
|
|
||||||
assert!(task.as_mut().poll(cx).is_pending());
|
|
||||||
Poll::Ready(())
|
|
||||||
}));
|
|
||||||
let classified = client_events(&observations)
|
|
||||||
.into_iter()
|
|
||||||
.filter(|event| {
|
|
||||||
matches!(
|
|
||||||
event,
|
|
||||||
ClientEvent::EchoLoss { .. }
|
|
||||||
| ClientEvent::LateReply { .. }
|
|
||||||
| ClientEvent::EchoReply { .. }
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
assert!(matches!(
|
|
||||||
classified.as_slice(),
|
|
||||||
[ClientEvent::EchoLoss { .. }, ClientEvent::LateReply { .. }]
|
|
||||||
));
|
|
||||||
drop(handle.stop());
|
|
||||||
let outcome = runtime.block_on(task);
|
|
||||||
let target = &outcome.recent_target_outcomes[0];
|
|
||||||
assert_eq!(target.replies_received, 0);
|
|
||||||
assert_eq!(target.late, 1);
|
|
||||||
server.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn retained_probe_deadline_shortens_after_late_reply() {
|
fn retained_probe_deadline_shortens_after_late_reply() {
|
||||||
let reply_gate = Arc::new(PacketGate::default());
|
let reply_gate = Arc::new(PacketGate::default());
|
||||||
|
|||||||
@@ -198,7 +198,6 @@ impl PendingMap {
|
|||||||
self.map.get_mut(&wire_seq).map(|entry| &mut entry.probe)
|
self.map.get_mut(&wire_seq).map(|entry| &mut entry.probe)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "tokio", test))]
|
|
||||||
pub fn next_timeout_deadline(&self) -> Option<Instant> {
|
pub fn next_timeout_deadline(&self) -> Option<Instant> {
|
||||||
self.first
|
self.first
|
||||||
.and_then(|wire_seq| self.map.get(&wire_seq))
|
.and_then(|wire_seq| self.map.get(&wire_seq))
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ impl SessionMachine {
|
|||||||
self.config.probe_timeout
|
self.config.probe_timeout
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn prepare_probe(&self) -> Result<Option<PreparedProbe>, ClientError> {
|
pub(crate) fn prepare_probe(&self) -> Result<PreparedProbe, ClientError> {
|
||||||
let session = self.open_session()?;
|
let session = self.open_session()?;
|
||||||
let bytes = encode_request(
|
let bytes = encode_request(
|
||||||
RequestToEncode::Echo {
|
RequestToEncode::Echo {
|
||||||
@@ -326,10 +326,10 @@ impl SessionMachine {
|
|||||||
},
|
},
|
||||||
self.config.hmac_key.as_deref(),
|
self.config.hmac_key.as_deref(),
|
||||||
)?;
|
)?;
|
||||||
Ok(Some(PreparedProbe {
|
Ok(PreparedProbe {
|
||||||
bytes: bytes.into_boxed_slice(),
|
bytes: bytes.into_boxed_slice(),
|
||||||
seq: session.next_wire_seq,
|
seq: session.next_wire_seq,
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn preflight_probe_commit(
|
pub(crate) fn preflight_probe_commit(
|
||||||
@@ -550,14 +550,21 @@ impl SessionMachine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn pending_is_empty(&self) -> bool {
|
pub(crate) fn negotiated_params(&self) -> Option<&NegotiatedParams> {
|
||||||
match &self.state {
|
match &self.state {
|
||||||
MachineState::Open(session) => session.pending.len() == 0,
|
MachineState::Open(session) => Some(&session.negotiated),
|
||||||
MachineState::NoTestCompleted | MachineState::Closed { .. } => true,
|
_ => None,
|
||||||
MachineState::Connected => false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn pending_is_empty(&self) -> bool {
|
||||||
|
match &self.state {
|
||||||
|
MachineState::Open(session) => session.pending.len() == 0,
|
||||||
|
_ => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
pub(crate) fn is_terminal(&self) -> bool {
|
pub(crate) fn is_terminal(&self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
self.state,
|
self.state,
|
||||||
@@ -575,14 +582,6 @@ impl SessionMachine {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn has_timed_out_metadata(&self) -> bool {
|
|
||||||
matches!(
|
|
||||||
&self.state,
|
|
||||||
MachineState::Open(session) if session.timed_out.len() > 0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(feature = "tokio", test))]
|
#[cfg(any(feature = "tokio", test))]
|
||||||
pub(crate) fn packets_sent(&self) -> u64 {
|
pub(crate) fn packets_sent(&self) -> u64 {
|
||||||
match &self.state {
|
match &self.state {
|
||||||
@@ -592,7 +591,6 @@ impl SessionMachine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "tokio")]
|
|
||||||
pub(crate) fn next_probe_timeout_deadline(&self) -> Option<Instant> {
|
pub(crate) fn next_probe_timeout_deadline(&self) -> Option<Instant> {
|
||||||
match &self.state {
|
match &self.state {
|
||||||
MachineState::Open(session) => session.pending.next_timeout_deadline(),
|
MachineState::Open(session) => session.pending.next_timeout_deadline(),
|
||||||
@@ -617,6 +615,7 @@ impl SessionMachine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
pub(crate) fn is_open(&self) -> bool {
|
pub(crate) fn is_open(&self) -> bool {
|
||||||
matches!(self.state, MachineState::Open(_))
|
matches!(self.state, MachineState::Open(_))
|
||||||
}
|
}
|
||||||
@@ -1178,43 +1177,6 @@ fn config_duration_to_ns(field: &str, duration: Duration) -> Result<i64, ClientE
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
impl SessionMachine {
|
|
||||||
pub(crate) fn seed_wrapped_probe_history_for_test(&mut self, now: ClientTimestamp) {
|
|
||||||
let session = self
|
|
||||||
.open_session_mut()
|
|
||||||
.expect("wrapped probe history requires an open session");
|
|
||||||
session.next_wire_seq = 0;
|
|
||||||
session.timed_out.insert(PendingProbe {
|
|
||||||
wire_seq: 0,
|
|
||||||
sent_at: now,
|
|
||||||
timeout_at: now.mono,
|
|
||||||
tx_not_before_wall: now.wall,
|
|
||||||
kernel_tx_timestamp: None,
|
|
||||||
});
|
|
||||||
session.completed.insert(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(all(test, feature = "tokio"))]
|
|
||||||
impl SessionMachine {
|
|
||||||
pub(crate) fn remove_pending_for_test(&mut self, wire_seq: u32) -> Option<PendingProbe> {
|
|
||||||
self.open_session_mut()
|
|
||||||
.expect("test pending probes require an open session")
|
|
||||||
.pending
|
|
||||||
.remove(wire_seq)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn replace_pending_for_test(&mut self, probe: PendingProbe) {
|
|
||||||
let session = self
|
|
||||||
.open_session_mut()
|
|
||||||
.expect("test pending probes require an open session");
|
|
||||||
session.pending.remove(probe.wire_seq);
|
|
||||||
session.pending.preflight_insert(probe.wire_seq).unwrap();
|
|
||||||
session.pending.commit_insert(probe);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -1744,7 +1706,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn uncommitted_probe_preparation_changes_no_authoritative_state() {
|
fn uncommitted_probe_preparation_changes_no_authoritative_state() {
|
||||||
let mut machine = open_machine(4, Duration::from_secs(1));
|
let mut machine = open_machine(4, Duration::from_secs(1));
|
||||||
let prepared = machine.prepare_probe().unwrap().unwrap();
|
let prepared = machine.prepare_probe().unwrap();
|
||||||
assert_eq!(prepared.seq, 0);
|
assert_eq!(prepared.seq, 0);
|
||||||
|
|
||||||
// Repeating preflight, and finalizing a commit that is never applied,
|
// Repeating preflight, and finalizing a commit that is never applied,
|
||||||
@@ -1768,8 +1730,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn stale_prepared_probe_is_rejected_without_changing_state() {
|
fn stale_prepared_probe_is_rejected_without_changing_state() {
|
||||||
let mut machine = open_machine(4, Duration::from_secs(1));
|
let mut machine = open_machine(4, Duration::from_secs(1));
|
||||||
let stale = machine.prepare_probe().unwrap().unwrap();
|
let stale = machine.prepare_probe().unwrap();
|
||||||
let accepted = machine.prepare_probe().unwrap().unwrap();
|
let accepted = machine.prepare_probe().unwrap();
|
||||||
let sent_at = timestamp(Instant::now());
|
let sent_at = timestamp(Instant::now());
|
||||||
let preflight = machine.preflight_probe_commit(&accepted).unwrap();
|
let preflight = machine.preflight_probe_commit(&accepted).unwrap();
|
||||||
let commit = machine.finalize_probe_commit(preflight, sent_at).unwrap();
|
let commit = machine.finalize_probe_commit(preflight, sent_at).unwrap();
|
||||||
@@ -1794,7 +1756,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn repeated_would_block_style_preflight_commits_once() {
|
fn repeated_would_block_style_preflight_commits_once() {
|
||||||
let mut machine = open_machine(4, Duration::from_secs(1));
|
let mut machine = open_machine(4, Duration::from_secs(1));
|
||||||
let prepared = machine.prepare_probe().unwrap().unwrap();
|
let prepared = machine.prepare_probe().unwrap();
|
||||||
let sent_at = timestamp(Instant::now());
|
let sent_at = timestamp(Instant::now());
|
||||||
|
|
||||||
for _ in 0..3 {
|
for _ in 0..3 {
|
||||||
@@ -1815,7 +1777,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn probe_commit_does_not_require_presentation_timing() {
|
fn probe_commit_does_not_require_presentation_timing() {
|
||||||
let mut machine = open_machine(4, Duration::from_secs(1));
|
let mut machine = open_machine(4, Duration::from_secs(1));
|
||||||
let prepared = machine.prepare_probe().unwrap().unwrap();
|
let prepared = machine.prepare_probe().unwrap();
|
||||||
let send_anchor = timestamp(Instant::now());
|
let send_anchor = timestamp(Instant::now());
|
||||||
let preflight = machine.preflight_probe_commit(&prepared).unwrap();
|
let preflight = machine.preflight_probe_commit(&prepared).unwrap();
|
||||||
let commit = machine
|
let commit = machine
|
||||||
@@ -1840,7 +1802,7 @@ mod tests {
|
|||||||
fn commit_uses_reserved_capacity_and_prevalidated_counter() {
|
fn commit_uses_reserved_capacity_and_prevalidated_counter() {
|
||||||
let mut machine = open_machine(2, Duration::from_secs(1));
|
let mut machine = open_machine(2, Duration::from_secs(1));
|
||||||
active_mut(&mut machine).packets_sent = u64::MAX - 1;
|
active_mut(&mut machine).packets_sent = u64::MAX - 1;
|
||||||
let prepared = machine.prepare_probe().unwrap().unwrap();
|
let prepared = machine.prepare_probe().unwrap();
|
||||||
let preflight = machine.preflight_probe_commit(&prepared).unwrap();
|
let preflight = machine.preflight_probe_commit(&prepared).unwrap();
|
||||||
let reserved_capacity = active(&machine).pending.capacity();
|
let reserved_capacity = active(&machine).pending.capacity();
|
||||||
assert_eq!(preflight.next_packets_sent, u64::MAX);
|
assert_eq!(preflight.next_packets_sent, u64::MAX);
|
||||||
@@ -1860,13 +1822,13 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn pending_capacity_exhaustion_is_detected_before_commit() {
|
fn pending_capacity_exhaustion_is_detected_before_commit() {
|
||||||
let mut machine = open_machine(1, Duration::from_secs(1));
|
let mut machine = open_machine(1, Duration::from_secs(1));
|
||||||
let first = machine.prepare_probe().unwrap().unwrap();
|
let first = machine.prepare_probe().unwrap();
|
||||||
let sent_at = timestamp(Instant::now());
|
let sent_at = timestamp(Instant::now());
|
||||||
let preflight = machine.preflight_probe_commit(&first).unwrap();
|
let preflight = machine.preflight_probe_commit(&first).unwrap();
|
||||||
let commit = machine.finalize_probe_commit(preflight, sent_at).unwrap();
|
let commit = machine.finalize_probe_commit(preflight, sent_at).unwrap();
|
||||||
machine.commit_probe_sent(commit, sent_at, first.bytes.len());
|
machine.commit_probe_sent(commit, sent_at, first.bytes.len());
|
||||||
|
|
||||||
let second = machine.prepare_probe().unwrap().unwrap();
|
let second = machine.prepare_probe().unwrap();
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
machine.preflight_probe_commit(&second),
|
machine.preflight_probe_commit(&second),
|
||||||
Err(ClientError::PendingLimitExceeded { limit: 1 })
|
Err(ClientError::PendingLimitExceeded { limit: 1 })
|
||||||
@@ -1877,14 +1839,14 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn pending_sequence_collision_is_detected_before_commit() {
|
fn pending_sequence_collision_is_detected_before_commit() {
|
||||||
let mut machine = open_machine(2, Duration::from_secs(1));
|
let mut machine = open_machine(2, Duration::from_secs(1));
|
||||||
let first = machine.prepare_probe().unwrap().unwrap();
|
let first = machine.prepare_probe().unwrap();
|
||||||
let sent_at = timestamp(Instant::now());
|
let sent_at = timestamp(Instant::now());
|
||||||
let preflight = machine.preflight_probe_commit(&first).unwrap();
|
let preflight = machine.preflight_probe_commit(&first).unwrap();
|
||||||
let commit = machine.finalize_probe_commit(preflight, sent_at).unwrap();
|
let commit = machine.finalize_probe_commit(preflight, sent_at).unwrap();
|
||||||
machine.commit_probe_sent(commit, sent_at, first.bytes.len());
|
machine.commit_probe_sent(commit, sent_at, first.bytes.len());
|
||||||
active_mut(&mut machine).next_wire_seq = 0;
|
active_mut(&mut machine).next_wire_seq = 0;
|
||||||
|
|
||||||
let reused = machine.prepare_probe().unwrap().unwrap();
|
let reused = machine.prepare_probe().unwrap();
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
machine.preflight_probe_commit(&reused),
|
machine.preflight_probe_commit(&reused),
|
||||||
Err(ClientError::PendingSequenceCollision { seq: 0 })
|
Err(ClientError::PendingSequenceCollision { seq: 0 })
|
||||||
@@ -1896,7 +1858,7 @@ mod tests {
|
|||||||
fn counter_overflow_is_detected_before_commit() {
|
fn counter_overflow_is_detected_before_commit() {
|
||||||
let mut machine = open_machine(2, Duration::from_secs(1));
|
let mut machine = open_machine(2, Duration::from_secs(1));
|
||||||
active_mut(&mut machine).packets_sent = u64::MAX;
|
active_mut(&mut machine).packets_sent = u64::MAX;
|
||||||
let prepared = machine.prepare_probe().unwrap().unwrap();
|
let prepared = machine.prepare_probe().unwrap();
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
machine.preflight_probe_commit(&prepared),
|
machine.preflight_probe_commit(&prepared),
|
||||||
@@ -1910,7 +1872,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn timeout_overflow_is_detected_before_commit() {
|
fn timeout_overflow_is_detected_before_commit() {
|
||||||
let mut machine = open_machine(2, Duration::MAX);
|
let mut machine = open_machine(2, Duration::MAX);
|
||||||
let prepared = machine.prepare_probe().unwrap().unwrap();
|
let prepared = machine.prepare_probe().unwrap();
|
||||||
let preflight = machine.preflight_probe_commit(&prepared).unwrap();
|
let preflight = machine.preflight_probe_commit(&prepared).unwrap();
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
@@ -1954,7 +1916,7 @@ mod tests {
|
|||||||
fn wrapping_sequence_from_max_to_zero_remains_valid() {
|
fn wrapping_sequence_from_max_to_zero_remains_valid() {
|
||||||
let mut machine = open_machine(2, Duration::from_secs(1));
|
let mut machine = open_machine(2, Duration::from_secs(1));
|
||||||
active_mut(&mut machine).next_wire_seq = u32::MAX;
|
active_mut(&mut machine).next_wire_seq = u32::MAX;
|
||||||
let prepared = machine.prepare_probe().unwrap().unwrap();
|
let prepared = machine.prepare_probe().unwrap();
|
||||||
let preflight = machine.preflight_probe_commit(&prepared).unwrap();
|
let preflight = machine.preflight_probe_commit(&prepared).unwrap();
|
||||||
let commit = machine
|
let commit = machine
|
||||||
.finalize_probe_commit(preflight, timestamp(Instant::now()))
|
.finalize_probe_commit(preflight, timestamp(Instant::now()))
|
||||||
@@ -1962,7 +1924,7 @@ mod tests {
|
|||||||
machine.commit_probe_sent(commit, timestamp(Instant::now()), prepared.bytes.len());
|
machine.commit_probe_sent(commit, timestamp(Instant::now()), prepared.bytes.len());
|
||||||
|
|
||||||
assert_eq!(active(&machine).next_wire_seq, 0);
|
assert_eq!(active(&machine).next_wire_seq, 0);
|
||||||
assert_eq!(machine.prepare_probe().unwrap().unwrap().seq, 0);
|
assert_eq!(machine.prepare_probe().unwrap().seq, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1982,7 +1944,7 @@ mod tests {
|
|||||||
session.timed_out.insert(obsolete);
|
session.timed_out.insert(obsolete);
|
||||||
session.completed.insert(0);
|
session.completed.insert(0);
|
||||||
|
|
||||||
let prepared = machine.prepare_probe().unwrap().unwrap();
|
let prepared = machine.prepare_probe().unwrap();
|
||||||
let preflight = machine.preflight_probe_commit(&prepared).unwrap();
|
let preflight = machine.preflight_probe_commit(&prepared).unwrap();
|
||||||
let commit = machine
|
let commit = machine
|
||||||
.finalize_probe_commit(preflight, timestamp(now))
|
.finalize_probe_commit(preflight, timestamp(now))
|
||||||
@@ -2395,7 +2357,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn send_probe(machine: &mut SessionMachine, sent_at: ClientTimestamp) -> u32 {
|
fn send_probe(machine: &mut SessionMachine, sent_at: ClientTimestamp) -> u32 {
|
||||||
let prepared = machine.prepare_probe().unwrap().unwrap();
|
let prepared = machine.prepare_probe().unwrap();
|
||||||
let preflight = machine.preflight_probe_commit(&prepared).unwrap();
|
let preflight = machine.preflight_probe_commit(&prepared).unwrap();
|
||||||
let commit = machine.finalize_probe_commit(preflight, sent_at).unwrap();
|
let commit = machine.finalize_probe_commit(preflight, sent_at).unwrap();
|
||||||
machine
|
machine
|
||||||
@@ -3520,9 +3482,7 @@ mod tests {
|
|||||||
machine: &mut SessionMachine,
|
machine: &mut SessionMachine,
|
||||||
now: ClientTimestamp,
|
now: ClientTimestamp,
|
||||||
) -> Result<ProbeSent, ClientError> {
|
) -> Result<ProbeSent, ClientError> {
|
||||||
let prepared = machine
|
let prepared = machine.prepare_probe()?;
|
||||||
.prepare_probe()?
|
|
||||||
.expect("prepare_probe always returns Some when Ok");
|
|
||||||
let preflight = machine.preflight_probe_commit(&prepared)?;
|
let preflight = machine.preflight_probe_commit(&prepared)?;
|
||||||
let commit = machine.finalize_probe_commit(preflight, now)?;
|
let commit = machine.finalize_probe_commit(preflight, now)?;
|
||||||
let bytes = prepared.bytes.len();
|
let bytes = prepared.bytes.len();
|
||||||
@@ -3630,10 +3590,7 @@ mod tests {
|
|||||||
result
|
result
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
let prepared_a = machine
|
let prepared_a = machine.prepare_probe().unwrap();
|
||||||
.prepare_probe()
|
|
||||||
.unwrap()
|
|
||||||
.expect("open session prepares probes");
|
|
||||||
prop_assert_eq!(prepared_a.seq, model.next_wire_seq);
|
prop_assert_eq!(prepared_a.seq, model.next_wire_seq);
|
||||||
|
|
||||||
let sent = do_send(machine, *now).expect(
|
let sent = do_send(machine, *now).expect(
|
||||||
@@ -3665,7 +3622,7 @@ mod tests {
|
|||||||
let result = machine.prepare_probe();
|
let result = machine.prepare_probe();
|
||||||
match model.state {
|
match model.state {
|
||||||
ModelState::Open => {
|
ModelState::Open => {
|
||||||
let prepared = result.unwrap().expect("open session prepares probes");
|
let prepared = result.unwrap();
|
||||||
prop_assert_eq!(prepared.seq, model.next_wire_seq);
|
prop_assert_eq!(prepared.seq, model.next_wire_seq);
|
||||||
prop_assert!(!prepared.bytes.is_empty());
|
prop_assert!(!prepared.bytes.is_empty());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,11 +58,11 @@ fn synthetic_probe_stream(count: u32) -> Vec<ClientEvent> {
|
|||||||
events.push(ClientEvent::EchoSent {
|
events.push(ClientEvent::EchoSent {
|
||||||
seq,
|
seq,
|
||||||
remote,
|
remote,
|
||||||
scheduled_at: sent_at.mono,
|
scheduled_at: Some(sent_at.mono),
|
||||||
sent_at,
|
sent_at,
|
||||||
bytes: 64,
|
bytes: 64,
|
||||||
send_call: Duration::from_micros(50),
|
send_call: Duration::from_micros(50),
|
||||||
timer_error: Duration::from_micros(20),
|
timer_error: Some(Duration::from_micros(20)),
|
||||||
});
|
});
|
||||||
|
|
||||||
if seq == count - 1 {
|
if seq == count - 1 {
|
||||||
|
|||||||
@@ -100,12 +100,14 @@ impl CoreStats {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_sent(&mut self, bytes: usize, send_call_ns: i128, timer_error_ns: i128) {
|
fn apply_sent(&mut self, bytes: usize, send_call_ns: i128, timer_error_ns: Option<i128>) {
|
||||||
self.events.sent_events += 1;
|
self.events.sent_events += 1;
|
||||||
self.packets.packets_sent += 1;
|
self.packets.packets_sent += 1;
|
||||||
self.packets.bytes_sent = self.packets.bytes_sent.saturating_add(bytes as u64);
|
self.packets.bytes_sent = self.packets.bytes_sent.saturating_add(bytes as u64);
|
||||||
self.send_call.push_ns(send_call_ns);
|
self.send_call.push_ns(send_call_ns);
|
||||||
self.timer_error.push_ns(timer_error_ns);
|
if let Some(error) = timer_error_ns {
|
||||||
|
self.timer_error.push_ns(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_unique_reply(&mut self, is_late: bool, sample: ReplySample) -> EventStatsUpdate {
|
fn apply_unique_reply(&mut self, is_late: bool, sample: ReplySample) -> EventStatsUpdate {
|
||||||
|
|||||||
@@ -89,11 +89,11 @@ impl StatsConfig {
|
|||||||
/// # ClientEvent::EchoSent {
|
/// # ClientEvent::EchoSent {
|
||||||
/// # seq,
|
/// # seq,
|
||||||
/// # remote: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 2112),
|
/// # remote: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 2112),
|
||||||
/// # scheduled_at: sent_at.mono,
|
/// # scheduled_at: Some(sent_at.mono),
|
||||||
/// # sent_at,
|
/// # sent_at,
|
||||||
/// # bytes: 64,
|
/// # bytes: 64,
|
||||||
/// # send_call: Duration::from_micros(send_call_us),
|
/// # send_call: Duration::from_micros(send_call_us),
|
||||||
/// # timer_error: Duration::from_micros(2),
|
/// # timer_error: Some(Duration::from_micros(2)),
|
||||||
/// # }
|
/// # }
|
||||||
/// # }
|
/// # }
|
||||||
/// #
|
/// #
|
||||||
@@ -142,11 +142,11 @@ impl StatsConfig {
|
|||||||
/// # ClientEvent::EchoSent {
|
/// # ClientEvent::EchoSent {
|
||||||
/// # seq,
|
/// # seq,
|
||||||
/// # remote: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 2112),
|
/// # remote: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 2112),
|
||||||
/// # scheduled_at: sent_at.mono,
|
/// # scheduled_at: Some(sent_at.mono),
|
||||||
/// # sent_at,
|
/// # sent_at,
|
||||||
/// # bytes: 64,
|
/// # bytes: 64,
|
||||||
/// # send_call: Duration::from_micros(send_call_us),
|
/// # send_call: Duration::from_micros(send_call_us),
|
||||||
/// # timer_error: Duration::from_micros(2),
|
/// # timer_error: Some(Duration::from_micros(2)),
|
||||||
/// # }
|
/// # }
|
||||||
/// # }
|
/// # }
|
||||||
/// #
|
/// #
|
||||||
@@ -418,11 +418,11 @@ impl StatsCollector {
|
|||||||
/// # ClientEvent::EchoSent {
|
/// # ClientEvent::EchoSent {
|
||||||
/// # seq,
|
/// # seq,
|
||||||
/// # remote: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 2112),
|
/// # remote: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 2112),
|
||||||
/// # scheduled_at: sent_at.mono,
|
/// # scheduled_at: Some(sent_at.mono),
|
||||||
/// # sent_at,
|
/// # sent_at,
|
||||||
/// # bytes: 64,
|
/// # bytes: 64,
|
||||||
/// # send_call: Duration::from_micros(send_call_us),
|
/// # send_call: Duration::from_micros(send_call_us),
|
||||||
/// # timer_error: Duration::from_micros(2),
|
/// # timer_error: Some(Duration::from_micros(2)),
|
||||||
/// # }
|
/// # }
|
||||||
/// # }
|
/// # }
|
||||||
/// #
|
/// #
|
||||||
@@ -475,11 +475,11 @@ impl StatsCollector {
|
|||||||
/// # ClientEvent::EchoSent {
|
/// # ClientEvent::EchoSent {
|
||||||
/// # seq,
|
/// # seq,
|
||||||
/// # remote: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 2112),
|
/// # remote: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 2112),
|
||||||
/// # scheduled_at: sent_at.mono,
|
/// # scheduled_at: Some(sent_at.mono),
|
||||||
/// # sent_at,
|
/// # sent_at,
|
||||||
/// # bytes: 64,
|
/// # bytes: 64,
|
||||||
/// # send_call: Duration::from_micros(send_call_us),
|
/// # send_call: Duration::from_micros(send_call_us),
|
||||||
/// # timer_error: Duration::from_micros(2),
|
/// # timer_error: Some(Duration::from_micros(2)),
|
||||||
/// # }
|
/// # }
|
||||||
/// # }
|
/// # }
|
||||||
/// #
|
/// #
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ pub(crate) enum StatsEvent {
|
|||||||
at: Instant,
|
at: Instant,
|
||||||
bytes: usize,
|
bytes: usize,
|
||||||
send_call_ns: i128,
|
send_call_ns: i128,
|
||||||
timer_error_ns: i128,
|
timer_error_ns: Option<i128>,
|
||||||
},
|
},
|
||||||
UniqueReply {
|
UniqueReply {
|
||||||
at: Instant,
|
at: Instant,
|
||||||
@@ -48,7 +48,7 @@ pub(crate) fn normalize_event(event: &ClientEvent) -> Option<StatsEvent> {
|
|||||||
at: sent_at.mono,
|
at: sent_at.mono,
|
||||||
bytes: *bytes,
|
bytes: *bytes,
|
||||||
send_call_ns: duration_ns_i128(*send_call),
|
send_call_ns: duration_ns_i128(*send_call),
|
||||||
timer_error_ns: duration_ns_i128(*timer_error),
|
timer_error_ns: timer_error.map(duration_ns_i128),
|
||||||
}),
|
}),
|
||||||
ClientEvent::EchoReply {
|
ClientEvent::EchoReply {
|
||||||
seq,
|
seq,
|
||||||
|
|||||||
@@ -146,11 +146,11 @@ mod tests {
|
|||||||
ClientEvent::EchoSent {
|
ClientEvent::EchoSent {
|
||||||
seq,
|
seq,
|
||||||
remote: "127.0.0.1:2112".parse().unwrap(),
|
remote: "127.0.0.1:2112".parse().unwrap(),
|
||||||
scheduled_at: sent_at.mono,
|
scheduled_at: Some(sent_at.mono),
|
||||||
sent_at,
|
sent_at,
|
||||||
bytes: 32,
|
bytes: 32,
|
||||||
send_call: Duration::from_micros(10 + u64::from(seq)),
|
send_call: Duration::from_micros(10 + u64::from(seq)),
|
||||||
timer_error: Duration::from_micros(2 + u64::from(seq)),
|
timer_error: Some(Duration::from_micros(2 + u64::from(seq))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,11 +34,11 @@ pub fn sent(seq: u32, sent_at: ClientTimestamp) -> ClientEvent {
|
|||||||
ClientEvent::EchoSent {
|
ClientEvent::EchoSent {
|
||||||
seq,
|
seq,
|
||||||
remote: "127.0.0.1:2112".parse().unwrap(),
|
remote: "127.0.0.1:2112".parse().unwrap(),
|
||||||
scheduled_at: sent_at.mono,
|
scheduled_at: Some(sent_at.mono),
|
||||||
sent_at,
|
sent_at,
|
||||||
bytes: 32,
|
bytes: 32,
|
||||||
send_call: Duration::from_micros(10),
|
send_call: Duration::from_micros(10),
|
||||||
timer_error: Duration::from_micros(2),
|
timer_error: Some(Duration::from_micros(2)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ pub fn sent_with_timings(
|
|||||||
unreachable!();
|
unreachable!();
|
||||||
};
|
};
|
||||||
*send_call = Duration::from_micros(send_call_us);
|
*send_call = Duration::from_micros(send_call_us);
|
||||||
*timer_error = Duration::from_micros(timer_error_us);
|
*timer_error = Some(Duration::from_micros(timer_error_us));
|
||||||
event
|
event
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -129,11 +129,11 @@ fn build_event(op: &Op, seq: u32, clock_ms: u64, base: Instant) -> ClientEvent {
|
|||||||
Op::Send => ClientEvent::EchoSent {
|
Op::Send => ClientEvent::EchoSent {
|
||||||
seq,
|
seq,
|
||||||
remote: ADDR.parse().unwrap(),
|
remote: ADDR.parse().unwrap(),
|
||||||
scheduled_at: ts_at(base, clock_ms).mono,
|
scheduled_at: Some(ts_at(base, clock_ms).mono),
|
||||||
sent_at: ts_at(base, clock_ms),
|
sent_at: ts_at(base, clock_ms),
|
||||||
bytes: 32,
|
bytes: 32,
|
||||||
send_call: Duration::from_micros(10),
|
send_call: Duration::from_micros(10),
|
||||||
timer_error: Duration::from_micros(2),
|
timer_error: Some(Duration::from_micros(2)),
|
||||||
},
|
},
|
||||||
Op::Reply { raw_ms } => {
|
Op::Reply { raw_ms } => {
|
||||||
let sent_at = ts_at(base, clock_ms.saturating_sub(u64::from(raw_ms)));
|
let sent_at = ts_at(base, clock_ms.saturating_sub(u64::from(raw_ms)));
|
||||||
|
|||||||
@@ -13,6 +13,27 @@ use irtt_stats::{
|
|||||||
EventStatsUpdate, IpdvPairUpdate, LateReplyMode, SampleMode, StatsCollector, StatsConfig,
|
EventStatsUpdate, IpdvPairUpdate, LateReplyMode, SampleMode, StatsCollector, StatsConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn caller_paced_sends_do_not_fabricate_timer_error_samples() {
|
||||||
|
let mut collector = StatsCollector::new(StatsConfig::finite());
|
||||||
|
let mut event = sent(0, ts(0));
|
||||||
|
let ClientEvent::EchoSent {
|
||||||
|
scheduled_at,
|
||||||
|
timer_error,
|
||||||
|
..
|
||||||
|
} = &mut event
|
||||||
|
else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
*scheduled_at = None;
|
||||||
|
*timer_error = None;
|
||||||
|
collector.process(&event);
|
||||||
|
let snapshot = collector.snapshot();
|
||||||
|
assert_eq!(snapshot.packets.packets_sent, 1);
|
||||||
|
assert_eq!(snapshot.send_call.count, 1);
|
||||||
|
assert_eq!(snapshot.timer_error.count, 0);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn exact_mode_reports_a_timer_error_median() {
|
fn exact_mode_reports_a_timer_error_median() {
|
||||||
let mut collector = StatsCollector::new(StatsConfig::finite());
|
let mut collector = StatsCollector::new(StatsConfig::finite());
|
||||||
|
|||||||
Reference in new issue
Block a user