test(managed): verify timeout budget fairness
This commit is contained in:
5 files changed
+457
-4
No files matched your search
@@ -773,6 +773,11 @@ impl AsyncClient {
|
|||||||
self.socket.poll_recv_ready(cx)
|
self.socket.poll_recv_ready(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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> {
|
pub(crate) fn skip_missed_probe_slots_at(&mut self, now: Instant) -> Result<(), ClientError> {
|
||||||
let Some(schedule) = self.schedule.as_mut() else {
|
let Some(schedule) = self.schedule.as_mut() else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ use irtt_proto::{
|
|||||||
use tokio::runtime::{Builder, Runtime};
|
use tokio::runtime::{Builder, Runtime};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{socket_options::tokio_socket_traffic_class, Client, RunMode, SocketConfig};
|
use crate::{
|
||||||
|
probe::PendingProbe, socket_options::tokio_socket_traffic_class, Client, ClientTimestamp,
|
||||||
|
RunMode, SocketConfig,
|
||||||
|
};
|
||||||
|
|
||||||
const TOKEN: u64 = 0x1234_5678_90ab_cdef;
|
const TOKEN: u64 = 0x1234_5678_90ab_cdef;
|
||||||
|
|
||||||
@@ -708,6 +711,35 @@ fn probe_false_readiness_resamples_all_timestamps() {
|
|||||||
assert_eq!(server.finish().len(), 2);
|
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),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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]
|
#[test]
|
||||||
fn successful_short_probe_commits_before_length_error() {
|
fn successful_short_probe_commits_before_length_error() {
|
||||||
let server = start_open_server(None, 0);
|
let server = start_open_server(None, 0);
|
||||||
|
|||||||
@@ -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},
|
time::{Duration, Instant, SystemTime},
|
||||||
};
|
};
|
||||||
|
|
||||||
use irtt_proto::{
|
use irtt_proto::{
|
||||||
@@ -16,7 +16,10 @@ use irtt_proto::{
|
|||||||
use tokio::runtime::{Builder, Runtime};
|
use tokio::runtime::{Builder, Runtime};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{socket::resolution_call_counts, ClientAuthConfig, NegotiationPolicy, RunMode};
|
use crate::{
|
||||||
|
probe::PendingProbe, socket::resolution_call_counts, ClientAuthConfig, ClientTimestamp,
|
||||||
|
NegotiationPolicy, RunMode,
|
||||||
|
};
|
||||||
|
|
||||||
const TOKEN: u64 = 0x1234_5678_90ab_cdef;
|
const TOKEN: u64 = 0x1234_5678_90ab_cdef;
|
||||||
|
|
||||||
@@ -318,6 +321,71 @@ fn client_events(
|
|||||||
.collect()
|
.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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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()
|
||||||
@@ -722,6 +790,7 @@ fn pruning_preserves_target_cursor_progress() {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
task.cursor = 5;
|
task.cursor = 5;
|
||||||
|
task.timeout_cursor = 5;
|
||||||
task.send_cursor = 5;
|
task.send_cursor = 5;
|
||||||
task.scan_remaining = 3;
|
task.scan_remaining = 3;
|
||||||
task.burst_remaining = 3;
|
task.burst_remaining = 3;
|
||||||
@@ -729,6 +798,7 @@ fn pruning_preserves_target_cursor_progress() {
|
|||||||
task.targets.truncate(2);
|
task.targets.truncate(2);
|
||||||
task.rebase_target_cursors();
|
task.rebase_target_cursors();
|
||||||
assert_eq!(task.cursor, 1);
|
assert_eq!(task.cursor, 1);
|
||||||
|
assert_eq!(task.timeout_cursor, 1);
|
||||||
assert_eq!(task.send_cursor, 1);
|
assert_eq!(task.send_cursor, 1);
|
||||||
assert_eq!(task.scan_remaining, 2);
|
assert_eq!(task.scan_remaining, 2);
|
||||||
assert_eq!(task.burst_remaining, 2);
|
assert_eq!(task.burst_remaining, 2);
|
||||||
@@ -737,6 +807,7 @@ fn pruning_preserves_target_cursor_progress() {
|
|||||||
task.targets.clear();
|
task.targets.clear();
|
||||||
task.rebase_target_cursors();
|
task.rebase_target_cursors();
|
||||||
assert_eq!(task.cursor, 0);
|
assert_eq!(task.cursor, 0);
|
||||||
|
assert_eq!(task.timeout_cursor, 0);
|
||||||
assert_eq!(task.send_cursor, 0);
|
assert_eq!(task.send_cursor, 0);
|
||||||
assert_eq!(task.scan_remaining, 0);
|
assert_eq!(task.scan_remaining, 0);
|
||||||
assert_eq!(task.burst_remaining, 0);
|
assert_eq!(task.burst_remaining, 0);
|
||||||
@@ -1653,6 +1724,335 @@ fn stagger_gate_tracks_active_membership() {
|
|||||||
second.finish();
|
second.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 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();
|
||||||
|
replace_pending_for_timeout_test(
|
||||||
|
&mut task,
|
||||||
|
0,
|
||||||
|
timeout_probe(0, now - Duration::from_secs(1), now),
|
||||||
|
);
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
replace_pending_for_timeout_test(
|
||||||
|
&mut task,
|
||||||
|
0,
|
||||||
|
timeout_probe(0, now - Duration::from_secs(1), now),
|
||||||
|
);
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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]
|
#[test]
|
||||||
fn overdue_queued_reply_is_lost_then_late() {
|
fn overdue_queued_reply_is_lost_then_late() {
|
||||||
let reply_gate = Arc::new(PacketGate::default());
|
let reply_gate = Arc::new(PacketGate::default());
|
||||||
|
|||||||
@@ -116,10 +116,12 @@ impl PendingMap {
|
|||||||
self.map.len()
|
self.map.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(feature = "tokio", test))]
|
||||||
pub fn next_timeout_deadline(&self) -> Option<Instant> {
|
pub fn next_timeout_deadline(&self) -> Option<Instant> {
|
||||||
self.deadlines.first().map(|key| key.timeout_at)
|
self.deadlines.first().map(|key| key.timeout_at)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(feature = "tokio", test))]
|
||||||
pub fn latest_timeout_deadline(&self) -> Option<Instant> {
|
pub fn latest_timeout_deadline(&self) -> Option<Instant> {
|
||||||
self.deadlines.last().map(|key| key.timeout_at)
|
self.deadlines.last().map(|key| key.timeout_at)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -414,7 +414,9 @@ impl SessionMachine {
|
|||||||
&mut self,
|
&mut self,
|
||||||
now: Instant,
|
now: Instant,
|
||||||
) -> Result<Vec<ClientEvent>, ClientError> {
|
) -> Result<Vec<ClientEvent>, ClientError> {
|
||||||
Ok(self.poll_timeouts_bounded_at(now, usize::MAX)?.events)
|
let batch = self.poll_timeouts_bounded_at(now, usize::MAX)?;
|
||||||
|
debug_assert!(!batch.more_due);
|
||||||
|
Ok(batch.events)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn poll_timeouts_bounded_at(
|
pub(crate) fn poll_timeouts_bounded_at(
|
||||||
@@ -1036,6 +1038,18 @@ impl SessionMachine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, feature = "tokio"))]
|
||||||
|
impl SessionMachine {
|
||||||
|
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::*;
|
||||||
|
|||||||
Reference in new issue
Block a user