fix(managed): bound opening receiver drains #46

Merged
Lochnair merged 2 commits from fix/managed-opening-drain into main 2026-08-04 07:39:23 +00:00
+629 -139
View File
@@ -42,6 +42,9 @@ use super::{
const GROUP_RECV_TIMEOUT: Duration = Duration::from_millis(20); const GROUP_RECV_TIMEOUT: Duration = Duration::from_millis(20);
const GROUP_FINAL_DRAIN: Duration = Duration::from_millis(100); const GROUP_FINAL_DRAIN: Duration = Duration::from_millis(100);
const MAX_SLEEP: Duration = Duration::from_millis(20); const MAX_SLEEP: Duration = Duration::from_millis(20);
// One receive may already be in progress, then the receiver gets one full
// receive interval after observing the request, plus one scheduler interval.
const GROUP_RECEIVER_DRAIN_LIMIT: Duration = Duration::from_millis(60);
const RECV_BUFFER_SIZE: usize = 65_536; const RECV_BUFFER_SIZE: usize = 65_536;
/// Maximum number of recent target outcomes retained in [`ManagedGroupOutcome`]. /// Maximum number of recent target outcomes retained in [`ManagedGroupOutcome`].
pub const MANAGED_GROUP_OUTCOME_HISTORY_LIMIT: usize = 256; pub const MANAGED_GROUP_OUTCOME_HISTORY_LIMIT: usize = 256;
@@ -385,6 +388,7 @@ fn classify_target_failure(error: &ClientError, opening: bool) -> ManagedTargetF
ClientError::InvalidConfig { .. } ClientError::InvalidConfig { .. }
| ClientError::OpenTimeoutTooSmall { .. } | ClientError::OpenTimeoutTooSmall { .. }
| ClientError::NoOpenTimeouts => ManagedTargetFailureKind::InvalidConfiguration, | ClientError::NoOpenTimeouts => ManagedTargetFailureKind::InvalidConfiguration,
ClientError::DurationOverflow if opening => ManagedTargetFailureKind::InvalidConfiguration,
ClientError::WorkerPanicked => ManagedTargetFailureKind::InternalWorker, ClientError::WorkerPanicked => ManagedTargetFailureKind::InternalWorker,
_ if opening => ManagedTargetFailureKind::OpeningProtocol, _ if opening => ManagedTargetFailureKind::OpeningProtocol,
_ => ManagedTargetFailureKind::RuntimeProtocol, _ => ManagedTargetFailureKind::RuntimeProtocol,
@@ -620,59 +624,99 @@ struct GroupShared {
family_remote: SocketAddr, family_remote: SocketAddr,
} }
#[derive(Debug, Default)] #[derive(Debug, Clone, Copy, Default)]
struct ReceiverDrainState { struct ReceiverDrainState {
requested: u64, requested: u64,
completed: u64, completed: u64,
deadline: Option<Instant>,
} }
impl ReceiverDrainState { impl ReceiverDrainState {
fn request_or_join(&mut self) -> Result<u64, ClientError> { fn request_or_join(&mut self, now: Instant) -> Result<u64, ClientError> {
if self.requested == self.completed { self.assert_invariants();
self.requested = self if self.is_outstanding() {
.requested return Ok(self.requested);
.checked_add(1)
.ok_or(ClientError::CounterOverflow {
counter: "receiver_drain_generation",
})?;
} }
Ok(self.requested)
let generation = self
.requested
.checked_add(1)
.ok_or(ClientError::CounterOverflow {
counter: "receiver_drain_generation",
})?;
let deadline = now
.checked_add(GROUP_RECEIVER_DRAIN_LIMIT)
.ok_or(ClientError::DurationOverflow)?;
self.requested = generation;
self.deadline = Some(deadline);
self.assert_invariants();
Ok(generation)
} }
fn complete_observed(&mut self, observed_generation: u64) -> bool { fn complete_observed(&mut self, observed_generation: u64) -> bool {
self.assert_invariants();
let observed_generation = observed_generation.min(self.requested); let observed_generation = observed_generation.min(self.requested);
if observed_generation > self.completed { if observed_generation > self.completed {
self.completed = observed_generation; self.completed = observed_generation;
if self.completed == self.requested {
self.deadline = None;
}
self.assert_invariants();
true true
} else { } else {
false false
} }
} }
fn complete_if_expired(&mut self, now: Instant) -> bool {
self.assert_invariants();
if !self.deadline.is_some_and(|deadline| deadline <= now) {
return false;
}
self.completed = self.requested;
self.deadline = None;
self.assert_invariants();
true
}
fn is_outstanding(&self) -> bool {
self.requested > self.completed
}
fn deadline(&self) -> Option<Instant> {
self.deadline
}
fn assert_invariants(&self) {
debug_assert!(self.completed <= self.requested);
debug_assert_eq!(self.is_outstanding(), self.deadline.is_some());
}
} }
#[cfg(test)] #[cfg(test)]
fn request_receiver_drain(shared: &GroupShared) -> Result<u64, ClientError> { fn request_receiver_drain(shared: &GroupShared, now: Instant) -> Result<u64, ClientError> {
shared shared
.receiver_drain .receiver_drain
.lock() .lock()
.expect("receiver drain mutex poisoned") .expect("receiver drain mutex poisoned")
.request_or_join() .request_or_join(now)
}
fn receiver_drain_snapshot(shared: &GroupShared) -> ReceiverDrainState {
*shared
.receiver_drain
.lock()
.expect("receiver drain mutex poisoned")
} }
fn requested_receiver_drain(shared: &GroupShared) -> u64 { fn requested_receiver_drain(shared: &GroupShared) -> u64 {
shared receiver_drain_snapshot(shared).requested
.receiver_drain
.lock()
.expect("receiver drain mutex poisoned")
.requested
} }
fn completed_receiver_drain(shared: &GroupShared) -> u64 { fn completed_receiver_drain(shared: &GroupShared) -> u64 {
shared receiver_drain_snapshot(shared).completed
.receiver_drain
.lock()
.expect("receiver drain mutex poisoned")
.completed
} }
fn complete_receiver_drain(shared: &GroupShared, observed_generation: u64) { fn complete_receiver_drain(shared: &GroupShared, observed_generation: u64) {
@@ -688,6 +732,14 @@ fn complete_receiver_drain(shared: &GroupShared, observed_generation: u64) {
} }
} }
fn complete_expired_receiver_drain(shared: &GroupShared, now: Instant) -> bool {
shared
.receiver_drain
.lock()
.expect("receiver drain mutex poisoned")
.complete_if_expired(now)
}
struct GroupSchedulerCleanup { struct GroupSchedulerCleanup {
shared: Arc<GroupShared>, shared: Arc<GroupShared>,
} }
@@ -934,6 +986,7 @@ fn run_group_scheduler(
} }
let now = Instant::now(); let now = Instant::now();
complete_expired_receiver_drain(&shared, now);
drive_open_attempts(&config.client, &socket, &shared, now); drive_open_attempts(&config.client, &socket, &shared, now);
poll_active_timeouts(&shared, now); poll_active_timeouts(&shared, now);
finish_completed_targets(&socket, &shared, now); finish_completed_targets(&socket, &shared, now);
@@ -945,23 +998,16 @@ fn run_group_scheduler(
let active = active_targets(&shared); let active = active_targets(&shared);
pacing.reconcile(&active, now); pacing.reconcile(&active, now);
if pacing.send_due(&active, now) { let receiver_drain_outstanding = receiver_drain_snapshot(&shared).is_outstanding();
match config.pacing { if send_due_active_probes(
ManagedGroupPacing::Staggered => { &config,
if let Some((target, scheduled_at)) = &socket,
pacing.next_staggered(&active, config.client.interval, now)? &shared,
{ &mut pacing,
send_echo_to_target(&socket, &shared, target, scheduled_at); &active,
} now,
} receiver_drain_outstanding,
ManagedGroupPacing::Burst => { )? {
if let Some(scheduled_at) = pacing.next_burst(config.client.interval, now)? {
for target in active {
send_echo_to_target(&socket, &shared, target, scheduled_at);
}
}
}
}
continue; continue;
} }
@@ -1009,9 +1055,6 @@ fn process_group_datagram(
datagram: ReceivedDatagramFrom, datagram: ReceivedDatagramFrom,
packet: &[u8], packet: &[u8],
) { ) {
// The single receiver thread cannot advance completion while processing
// this datagram, so one snapshot consistently classifies its target.
let completed_drain_generation = completed_receiver_drain(shared);
let target = { let target = {
let registry = shared let registry = shared
.registry .registry
@@ -1026,87 +1069,108 @@ fn process_group_datagram(
return; return;
}; };
let mut wake_scheduler = false; let is_opening = matches!(
{ target.lock().expect("target mutex poisoned").status,
let mut target = target.lock().expect("target mutex poisoned"); TargetStatus::Opening { .. }
match target.status { );
TargetStatus::Opening { if is_opening {
attempt, if process_opening_target_packet(socket, shared, &target, datagram.received_at, packet) {
awaiting_receiver_generation, let _ = shared.control_tx.send(ControlMessage::Wake);
..
} => {
if attempt == 0
|| awaiting_receiver_generation
.is_some_and(|generation| completed_drain_generation >= generation)
{
return;
}
let opened_at = datagram.received_at;
let reply = match target.runtime.inspect_open_datagram(packet) {
Ok(OpenDatagramDisposition::Ignore) => return,
Ok(OpenDatagramDisposition::Trusted(reply)) => reply,
Err(err) => {
target.mark_finished(ManagedTargetEndReason::OpenFailed(
ManagedTargetFailure::opening(&err),
));
let _ = shared.control_tx.send(ControlMessage::Wake);
return;
}
};
let machine = match target.runtime.prepare_open_acceptance(reply, opened_at) {
Ok(machine) => machine,
Err(failure) => {
send_group_cleanup_close_best_effort(
socket,
target.remote,
failure.cleanup_close.as_deref(),
);
target.mark_finished(ManagedTargetEndReason::OpenFailed(
ManagedTargetFailure::opening(&failure.primary),
));
let _ = shared.control_tx.send(ControlMessage::Wake);
return;
}
};
match prepare_group_open(shared, machine, opened_at) {
Ok(prepared) => {
let outcome = target.runtime.commit_open(prepared.machine);
target.schedule = prepared.schedule;
publish_open_outcome(&shared.hub, &mut target, outcome);
wake_scheduler = true;
}
Err(failure) => {
send_group_cleanup_close_best_effort(
socket,
target.remote,
failure.machine.cleanup_close_packet(),
);
target.mark_finished(ManagedTargetEndReason::OpenFailed(
ManagedTargetFailure::opening(&failure.primary),
));
wake_scheduler = true;
}
}
}
TargetStatus::Active | TargetStatus::Draining { .. } => {
wake_scheduler = process_active_target_packet(
&shared.hub,
&mut target,
packet,
datagram.received_at,
datagram.meta,
);
}
TargetStatus::Finished => {}
} }
return;
} }
let mut target = target.lock().expect("target mutex poisoned");
let wake_scheduler = match target.status {
TargetStatus::Active | TargetStatus::Draining { .. } => process_active_target_packet(
&shared.hub,
&mut target,
packet,
datagram.received_at,
datagram.meta,
),
TargetStatus::Opening { .. } | TargetStatus::Finished => false,
};
if wake_scheduler { if wake_scheduler {
let _ = shared.control_tx.send(ControlMessage::Wake); let _ = shared.control_tx.send(ControlMessage::Wake);
} }
} }
fn process_opening_target_packet(
socket: &UdpSocket,
shared: &GroupShared,
target: &Arc<Mutex<TargetState>>,
opened_at: ClientTimestamp,
packet: &[u8],
) -> bool {
// Serialize opening-packet eligibility with natural and forced drain
// completion. The shared drain lock always precedes the target lock.
let drain = shared
.receiver_drain
.lock()
.expect("receiver drain mutex poisoned");
let mut target = target.lock().expect("target mutex poisoned");
let TargetStatus::Opening {
attempt,
awaiting_receiver_generation,
..
} = target.status
else {
return false;
};
if attempt == 0
|| awaiting_receiver_generation.is_some_and(|generation| drain.completed >= generation)
{
return false;
}
let reply = match target.runtime.inspect_open_datagram(packet) {
Ok(OpenDatagramDisposition::Ignore) => return false,
Ok(OpenDatagramDisposition::Trusted(reply)) => reply,
Err(err) => {
target.mark_finished(ManagedTargetEndReason::OpenFailed(
ManagedTargetFailure::opening(&err),
));
return true;
}
};
let machine = match target.runtime.prepare_open_acceptance(reply, opened_at) {
Ok(machine) => machine,
Err(failure) => {
send_group_cleanup_close_best_effort(
socket,
target.remote,
failure.cleanup_close.as_deref(),
);
target.mark_finished(ManagedTargetEndReason::OpenFailed(
ManagedTargetFailure::opening(&failure.primary),
));
return true;
}
};
match prepare_group_open(shared, machine, opened_at) {
Ok(prepared) => {
let outcome = target.runtime.commit_open(prepared.machine);
target.schedule = prepared.schedule;
publish_open_outcome(&shared.hub, &mut target, outcome);
}
Err(failure) => {
send_group_cleanup_close_best_effort(
socket,
target.remote,
failure.machine.cleanup_close_packet(),
);
target.mark_finished(ManagedTargetEndReason::OpenFailed(
ManagedTargetFailure::opening(&failure.primary),
));
}
}
drop(target);
drop(drain);
true
}
fn validate_group_negotiation( fn validate_group_negotiation(
shared: &GroupShared, shared: &GroupShared,
negotiated: &crate::NegotiatedParams, negotiated: &crate::NegotiatedParams,
@@ -1498,7 +1562,7 @@ fn register_due_open_drains(
if attempt == 0 || next_send_at > now { if attempt == 0 || next_send_at > now {
continue; continue;
} }
match drain.request_or_join() { match drain.request_or_join(now) {
Ok(generation) => { Ok(generation) => {
target.status = TargetStatus::Opening { target.status = TargetStatus::Opening {
attempt, attempt,
@@ -1572,6 +1636,38 @@ fn finish_completed_targets(socket: &UdpSocket, shared: &GroupShared, now: Insta
} }
} }
fn send_due_active_probes(
config: &ManagedClientGroupConfig,
socket: &UdpSocket,
shared: &GroupShared,
pacing: &mut PacingRuntime,
active: &[Arc<Mutex<TargetState>>],
now: Instant,
receiver_drain_outstanding: bool,
) -> Result<bool, ClientError> {
if receiver_drain_outstanding || !pacing.send_due(active, now) {
return Ok(false);
}
match config.pacing {
ManagedGroupPacing::Staggered => {
if let Some((target, scheduled_at)) =
pacing.next_staggered(active, config.client.interval, now)?
{
send_echo_to_target(socket, shared, target, scheduled_at);
}
}
ManagedGroupPacing::Burst => {
if let Some(scheduled_at) = pacing.next_burst(config.client.interval, now)? {
for target in active {
send_echo_to_target(socket, shared, target.clone(), scheduled_at);
}
}
}
}
Ok(true)
}
fn send_echo_to_target( fn send_echo_to_target(
socket: &UdpSocket, socket: &UdpSocket,
shared: &GroupShared, shared: &GroupShared,
@@ -2083,15 +2179,38 @@ fn wait_for_next_scheduler_wakeup(
) -> Option<ControlMessage> { ) -> Option<ControlMessage> {
let open_deadline = next_open_deadline(shared); let open_deadline = next_open_deadline(shared);
let pacing_deadline = (!active.is_empty()).then(|| pacing.next_wakeup()).flatten(); let pacing_deadline = (!active.is_empty()).then(|| pacing.next_wakeup()).flatten();
let drain_deadline = next_drain_deadline(shared); let target_drain_deadline = next_target_drain_deadline(shared);
let deadline = [open_deadline, pacing_deadline, drain_deadline] let receiver_drain = receiver_drain_snapshot(shared);
.into_iter() let deadline = next_scheduler_deadline(
.flatten() open_deadline,
.min(); pacing_deadline,
target_drain_deadline,
receiver_drain,
);
wait_for_scheduler_control(control_rx, &shared.cancellation, deadline) wait_for_scheduler_control(control_rx, &shared.cancellation, deadline)
} }
fn next_scheduler_deadline(
open_deadline: Option<Instant>,
pacing_deadline: Option<Instant>,
target_drain_deadline: Option<Instant>,
receiver_drain: ReceiverDrainState,
) -> Option<Instant> {
let pacing_deadline = (!receiver_drain.is_outstanding())
.then_some(pacing_deadline)
.flatten();
[
open_deadline,
pacing_deadline,
target_drain_deadline,
receiver_drain.deadline(),
]
.into_iter()
.flatten()
.min()
}
fn wait_for_scheduler_control( fn wait_for_scheduler_control(
control_rx: &mpsc::Receiver<ControlMessage>, control_rx: &mpsc::Receiver<ControlMessage>,
cancellation: &CancellationToken, cancellation: &CancellationToken,
@@ -2139,7 +2258,7 @@ fn next_open_deadline(shared: &GroupShared) -> Option<Instant> {
.min() .min()
} }
fn next_drain_deadline(shared: &GroupShared) -> Option<Instant> { fn next_target_drain_deadline(shared: &GroupShared) -> Option<Instant> {
all_targets(shared) all_targets(shared)
.into_iter() .into_iter()
.filter_map(|target| { .filter_map(|target| {
@@ -3983,6 +4102,18 @@ mod tests {
)); ));
} }
#[test]
fn opening_duration_overflow_is_invalid_configuration_only_while_opening() {
assert_eq!(
classify_target_failure(&ClientError::DurationOverflow, true),
ManagedTargetFailureKind::InvalidConfiguration
);
assert_eq!(
classify_target_failure(&ClientError::DurationOverflow, false),
ManagedTargetFailureKind::RuntimeProtocol
);
}
#[test] #[test]
fn group_open_deadline_overflow_fails_before_send() { fn group_open_deadline_overflow_fails_before_send() {
let silent_peer = UdpSocket::bind("127.0.0.1:0").unwrap(); let silent_peer = UdpSocket::bind("127.0.0.1:0").unwrap();
@@ -3994,14 +4125,41 @@ mod tests {
); );
config.client.open_timeouts = vec![Duration::MAX]; config.client.open_timeouts = vec![Duration::MAX];
let outcome = ManagedClientGroup::start(config, vec![target("peer", remote)]) let (session, sub) = ManagedClientGroup::start_with_subscription(
.unwrap() config,
.join() vec![target("peer", remote)],
.unwrap(); SubscriberConfig {
capacity: 8,
overflow: SubscriberOverflow::DropNewest,
},
)
.unwrap();
let events = collect_group_events_until_disconnected(&sub);
let outcome = session.join().unwrap();
assert_eq!(outcome.total_target_outcomes, 1);
assert_eq!(outcome.failed_target_outcomes, 1); assert_eq!(outcome.failed_target_outcomes, 1);
assert_eq!(outcome.targets.len(), 1);
assert!(matches!(
outcome.targets[0].end_reason,
ManagedTargetEndReason::OpenFailed(ManagedTargetFailure {
kind: ManagedTargetFailureKind::InvalidConfiguration,
..
})
));
let failure = outcome.targets[0].end_reason.failure().unwrap(); let failure = outcome.targets[0].end_reason.failure().unwrap();
assert_eq!(failure.kind, ManagedTargetFailureKind::InvalidConfiguration);
assert_ne!(failure.kind, ManagedTargetFailureKind::OpeningProtocol);
assert!(failure.message.contains("duration")); assert!(failure.message.contains("duration"));
let finished: Vec<_> = events
.iter()
.filter_map(|event| match event {
ManagedGroupEvent::TargetFinished(outcome) => Some(outcome),
ManagedGroupEvent::Client(_) => None,
})
.collect();
assert_eq!(finished.len(), 1);
assert_eq!(finished[0], &outcome.targets[0]);
silent_peer.set_nonblocking(true).unwrap(); silent_peer.set_nonblocking(true).unwrap();
let mut packet = [0_u8; 512]; let mut packet = [0_u8; 512];
assert!(matches!( assert!(matches!(
@@ -4022,7 +4180,7 @@ mod tests {
)); ));
let (shared, control_rx) = shared_with_targets(&config, EventHub::new(), vec![target]); let (shared, control_rx) = shared_with_targets(&config, EventHub::new(), vec![target]);
let shared = Arc::new(shared); let shared = Arc::new(shared);
let generation = request_receiver_drain(&shared).unwrap(); let generation = request_receiver_drain(&shared, now).unwrap();
let waiter_shared = shared.clone(); let waiter_shared = shared.clone();
let waiter = thread::spawn(move || { let waiter = thread::spawn(move || {
wait_for_scheduler_control(&control_rx, &waiter_shared.cancellation, None) wait_for_scheduler_control(&control_rx, &waiter_shared.cancellation, None)
@@ -4037,6 +4195,95 @@ mod tests {
assert_eq!(completed_receiver_drain(&shared), generation); assert_eq!(completed_receiver_drain(&shared), generation);
} }
#[test]
fn receiver_drain_request_join_and_natural_completion_share_one_deadline() {
let requested_at = Instant::now();
let mut drain = ReceiverDrainState::default();
let generation = drain.request_or_join(requested_at).unwrap();
let deadline = drain.deadline().expect("new drain needs a deadline");
assert_eq!(generation, 1);
assert_eq!(deadline, requested_at + GROUP_RECEIVER_DRAIN_LIMIT);
assert!(drain.is_outstanding());
assert_eq!(
drain
.request_or_join(requested_at + Duration::from_millis(10))
.unwrap(),
generation
);
assert_eq!(drain.deadline(), Some(deadline));
assert!(drain.complete_observed(generation));
assert_eq!(drain.completed, generation);
assert_eq!(drain.deadline(), None);
assert!(!drain.is_outstanding());
}
#[test]
fn receiver_drain_forced_completion_is_bounded_and_idempotent() {
let requested_at = Instant::now();
let mut drain = ReceiverDrainState::default();
let generation = drain.request_or_join(requested_at).unwrap();
let deadline = drain.deadline().unwrap();
assert!(!drain.complete_if_expired(deadline - Duration::from_nanos(1)));
assert_eq!(drain.completed, 0);
assert_eq!(drain.deadline(), Some(deadline));
assert!(drain.complete_if_expired(deadline));
assert_eq!(drain.completed, generation);
assert_eq!(drain.deadline(), None);
assert!(!drain.complete_if_expired(deadline));
assert!(!drain.complete_observed(generation));
let later_request = deadline + Duration::from_millis(10);
let next_generation = drain.request_or_join(later_request).unwrap();
let next_deadline = drain.deadline().unwrap();
assert_eq!(next_generation, generation + 1);
assert_eq!(next_deadline, later_request + GROUP_RECEIVER_DRAIN_LIMIT);
assert!(drain.complete_if_expired(next_deadline + Duration::from_nanos(1)));
assert_eq!(drain.completed, next_generation);
}
#[test]
fn stale_receiver_completion_cannot_clear_a_newer_generation_deadline() {
let requested_at = Instant::now();
let mut drain = ReceiverDrainState::default();
let first = drain.request_or_join(requested_at).unwrap();
assert!(drain.complete_if_expired(requested_at + GROUP_RECEIVER_DRAIN_LIMIT));
let second_requested_at = requested_at + GROUP_RECEIVER_DRAIN_LIMIT;
let second = drain.request_or_join(second_requested_at).unwrap();
let second_deadline = drain.deadline().unwrap();
assert_eq!(second, first + 1);
assert!(!drain.complete_observed(first));
assert_eq!(drain.completed, first);
assert_eq!(drain.requested, second);
assert_eq!(drain.deadline(), Some(second_deadline));
assert!(drain.is_outstanding());
}
#[test]
fn receiver_drain_generation_overflow_does_not_mutate_state() {
let mut drain = ReceiverDrainState {
requested: u64::MAX,
completed: u64::MAX,
deadline: None,
};
let before = drain;
assert!(matches!(
drain.request_or_join(Instant::now()),
Err(ClientError::CounterOverflow {
counter: "receiver_drain_generation"
})
));
assert_eq!(drain.requested, before.requested);
assert_eq!(drain.completed, before.completed);
assert_eq!(drain.deadline(), before.deadline());
}
#[test] #[test]
fn expired_attempt_waits_for_receiver_drain_before_retrying() { fn expired_attempt_waits_for_receiver_drain_before_retrying() {
let interval = Duration::from_millis(10); let interval = Duration::from_millis(10);
@@ -4078,8 +4325,22 @@ mod tests {
}; };
assert!(recv_request_timeout(&peer).is_none()); assert!(recv_request_timeout(&peer).is_none());
complete_receiver_drain(&shared, generation); let drain_deadline = receiver_drain_snapshot(&shared).deadline().unwrap();
drive_open_attempts(&config, &socket, &shared, deadline); assert!(!complete_expired_receiver_drain(
&shared,
drain_deadline - Duration::from_nanos(1)
));
drive_open_attempts(
&config,
&socket,
&shared,
drain_deadline - Duration::from_nanos(1),
);
assert!(target.lock().unwrap().final_reason.is_none());
assert!(complete_expired_receiver_drain(&shared, drain_deadline));
assert_eq!(completed_receiver_drain(&shared), generation);
drive_open_attempts(&config, &socket, &shared, drain_deadline);
assert!(matches!( assert!(matches!(
target.lock().expect("target mutex poisoned").status, target.lock().expect("target mutex poisoned").status,
TargetStatus::Opening { TargetStatus::Opening {
@@ -4091,6 +4352,181 @@ mod tests {
assert!(recv_request_timeout(&peer).is_some()); assert!(recv_request_timeout(&peer).is_some());
} }
#[test]
fn continuous_traffic_receiver_drain_forces_progress_and_resumes_pacing() {
let interval = Duration::from_millis(10);
let mut config = group_config(None, interval, ManagedGroupPacing::Staggered);
config.client.open_timeouts = vec![Duration::from_millis(200), Duration::from_millis(200)];
let params = test_params(None, interval);
let active_peer = UdpSocket::bind("127.0.0.1:0").unwrap();
let opening_peer = UdpSocket::bind("127.0.0.1:0").unwrap();
for peer in [&active_peer, &opening_peer] {
peer.set_read_timeout(Some(Duration::from_millis(100)))
.unwrap();
}
let started_at = Instant::now();
let active = Arc::new(Mutex::new(active_probe_target(
&config.client,
&params,
active_peer.local_addr().unwrap(),
ClientTimestamp {
wall: std::time::SystemTime::now(),
mono: started_at,
},
)));
let opening = Arc::new(Mutex::new(
TargetState::new(
&config.client,
target("opening", opening_peer.local_addr().unwrap()),
1,
started_at,
)
.unwrap(),
));
let (shared, _control_rx) = shared_with_targets(
&config.client,
EventHub::new(),
vec![active.clone(), opening.clone()],
);
let socket = UdpSocket::bind("127.0.0.1:0").unwrap();
drive_open_attempts(&config.client, &socket, &shared, started_at);
assert!(recv_request_timeout(&opening_peer).is_some());
let opening_deadline = match opening.lock().unwrap().status {
TargetStatus::Opening { next_send_at, .. } => next_send_at,
ref status => panic!("unexpected opening status: {status:?}"),
};
drive_open_attempts(&config.client, &socket, &shared, opening_deadline);
let drain = receiver_drain_snapshot(&shared);
let drain_deadline = drain.deadline().expect("expired attempt needs a drain");
assert!(drain.is_outstanding());
let active_targets = active_targets(&shared);
let mut pacing = PacingRuntime::new(config.pacing);
pacing.reconcile(&active_targets, opening_deadline);
assert!(!send_due_active_probes(
&config,
&socket,
&shared,
&mut pacing,
&active_targets,
opening_deadline,
true,
)
.unwrap());
assert!(recv_request_timeout(&active_peer).is_none());
let unrelated = UdpSocket::bind("127.0.0.1:0").unwrap();
for _ in 0..3 {
unrelated
.send_to(&[0_u8], socket.local_addr().unwrap())
.unwrap();
let (datagram, packet) = recv_group_datagram(&socket);
process_group_datagram(&socket, &shared, datagram, &packet);
assert_eq!(completed_receiver_drain(&shared), drain.completed);
}
assert!(!complete_expired_receiver_drain(
&shared,
drain_deadline - Duration::from_nanos(1)
));
drive_open_attempts(
&config.client,
&socket,
&shared,
drain_deadline - Duration::from_nanos(1),
);
assert!(recv_request_timeout(&opening_peer).is_none());
assert!(complete_expired_receiver_drain(&shared, drain_deadline));
drive_open_attempts(&config.client, &socket, &shared, drain_deadline);
assert!(recv_request_timeout(&opening_peer).is_some());
drive_open_attempts(&config.client, &socket, &shared, drain_deadline);
assert!(recv_request_timeout(&opening_peer).is_none());
assert!(send_due_active_probes(
&config,
&socket,
&shared,
&mut pacing,
&active_targets,
drain_deadline,
receiver_drain_snapshot(&shared).is_outstanding(),
)
.unwrap());
assert!(recv_request_timeout(&active_peer).is_some());
assert!(!send_due_active_probes(
&config,
&socket,
&shared,
&mut pacing,
&active_targets,
drain_deadline,
false,
)
.unwrap());
assert!(recv_request_timeout(&active_peer).is_none());
assert_eq!(active.lock().unwrap().runtime.packets_sent(), 1);
assert!(pacing
.next_wakeup()
.is_some_and(|deadline| deadline > drain_deadline));
}
#[test]
fn active_reply_and_timeout_processing_continue_during_receiver_drain() {
let peer = UdpSocket::bind("127.0.0.1:0").unwrap();
let fixture = active_target_fixture_at(peer.local_addr().unwrap());
let shared = shared_with_target(
&fixture.client_config,
fixture.hub.clone(),
fixture.target.clone(),
);
let socket = UdpSocket::bind("127.0.0.1:0").unwrap();
request_receiver_drain(&shared, Instant::now()).unwrap();
peer.send_to(&fixture.close_reply, socket.local_addr().unwrap())
.unwrap();
let (datagram, packet) = recv_group_datagram(&socket);
process_group_datagram(&socket, &shared, datagram, &packet);
assert!(receiver_drain_snapshot(&shared).is_outstanding());
assert_eq!(
fixture.target.lock().unwrap().final_reason,
Some(ManagedTargetEndReason::PeerClosed)
);
assert!(drain_available_group_events(&fixture.subscription)
.iter()
.any(|event| matches!(
event,
ManagedGroupEvent::Client(TargetEvent {
event: ClientEvent::EchoReply { seq: 0, .. },
..
})
)));
let fixture = active_target_fixture();
let shared =
shared_with_target(&fixture.client_config, fixture.hub.clone(), fixture.target);
let now = Instant::now();
request_receiver_drain(&shared, now).unwrap();
poll_active_timeouts(
&shared,
now + fixture.client_config.probe_timeout + Duration::from_millis(10),
);
assert!(receiver_drain_snapshot(&shared).is_outstanding());
assert!(drain_available_group_events(&fixture.subscription)
.iter()
.any(|event| matches!(
event,
ManagedGroupEvent::Client(TargetEvent {
event: ClientEvent::EchoLoss { seq: 0, .. },
..
})
)));
}
#[test] #[test]
fn queued_reply_dequeued_after_deadline_opens_without_retry() { fn queued_reply_dequeued_after_deadline_opens_without_retry() {
let interval = Duration::from_millis(10); let interval = Duration::from_millis(10);
@@ -4162,7 +4598,7 @@ mod tests {
let shared = shared_with_target(&config, EventHub::new(), target); let shared = shared_with_target(&config, EventHub::new(), target);
let observed_before_request = requested_receiver_drain(&shared); let observed_before_request = requested_receiver_drain(&shared);
let generation = request_receiver_drain(&shared).unwrap(); let generation = request_receiver_drain(&shared, Instant::now()).unwrap();
complete_receiver_drain(&shared, observed_before_request); complete_receiver_drain(&shared, observed_before_request);
assert!(completed_receiver_drain(&shared) < generation); assert!(completed_receiver_drain(&shared) < generation);
@@ -4257,7 +4693,7 @@ mod tests {
} }
#[test] #[test]
fn post_drain_reply_cannot_open_expired_attempt() { fn force_completed_drain_rejects_packet_dequeued_before_completion() {
let interval = Duration::from_millis(10); let interval = Duration::from_millis(10);
let mut config = group_config(None, interval, ManagedGroupPacing::Staggered).client; let mut config = group_config(None, interval, ManagedGroupPacing::Staggered).client;
config.open_timeouts = vec![Duration::from_millis(200), Duration::from_millis(200)]; config.open_timeouts = vec![Duration::from_millis(200), Duration::from_millis(200)];
@@ -4287,14 +4723,15 @@ mod tests {
} => generation, } => generation,
ref status => panic!("unexpected waiting status: {status:?}"), ref status => panic!("unexpected waiting status: {status:?}"),
}; };
complete_receiver_drain(&shared, generation);
peer.send_to( peer.send_to(
&open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, &params), &open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, &params),
client_addr, client_addr,
) )
.unwrap(); .unwrap();
let (datagram, packet) = recv_group_datagram(&socket); let (datagram, packet) = recv_group_datagram(&socket);
let drain_deadline = receiver_drain_snapshot(&shared).deadline().unwrap();
assert!(complete_expired_receiver_drain(&shared, drain_deadline));
assert_eq!(completed_receiver_drain(&shared), generation);
process_group_datagram(&socket, &shared, datagram, &packet); process_group_datagram(&socket, &shared, datagram, &packet);
{ {
let target = target.lock().expect("target mutex poisoned"); let target = target.lock().expect("target mutex poisoned");
@@ -4309,7 +4746,7 @@ mod tests {
assert!(target.runtime.prepare_open_request().is_ok()); assert!(target.runtime.prepare_open_request().is_ok());
} }
drive_open_attempts(&config, &socket, &shared, deadline); drive_open_attempts(&config, &socket, &shared, drain_deadline);
assert!(matches!( assert!(matches!(
target.lock().expect("target mutex poisoned").status, target.lock().expect("target mutex poisoned").status,
TargetStatus::Opening { TargetStatus::Opening {
@@ -4379,9 +4816,17 @@ mod tests {
ref status => panic!("unexpected waiting status: {status:?}"), ref status => panic!("unexpected waiting status: {status:?}"),
}); });
assert_eq!(generations[0], generations[1]); assert_eq!(generations[0], generations[1]);
let shared_drain = receiver_drain_snapshot(&shared);
assert_eq!(
shared_drain.deadline(),
Some(deadline + GROUP_RECEIVER_DRAIN_LIMIT)
);
complete_receiver_drain(&shared, generations[0]); assert!(complete_expired_receiver_drain(
drive_open_attempts(&config, &socket, &shared, deadline); &shared,
shared_drain.deadline().unwrap()
));
drive_open_attempts(&config, &socket, &shared, shared_drain.deadline().unwrap());
for target in [&first, &second] { for target in [&first, &second] {
assert!(matches!( assert!(matches!(
target.lock().unwrap().status, target.lock().unwrap().status,
@@ -4516,8 +4961,22 @@ mod tests {
generation generation
}; };
complete_receiver_drain(&shared, generation); let drain_deadline = receiver_drain_snapshot(&shared).deadline().unwrap();
drive_open_attempts(&config, &socket, &shared, deadline); assert!(!complete_expired_receiver_drain(
&shared,
drain_deadline - Duration::from_nanos(1)
));
drive_open_attempts(
&config,
&socket,
&shared,
drain_deadline - Duration::from_nanos(1),
);
assert!(target.lock().unwrap().final_reason.is_none());
assert!(complete_expired_receiver_drain(&shared, drain_deadline));
assert_eq!(completed_receiver_drain(&shared), generation);
drive_open_attempts(&config, &socket, &shared, drain_deadline);
let reason = { let reason = {
let target = target.lock().expect("target mutex poisoned"); let target = target.lock().expect("target mutex poisoned");
assert!(matches!(target.status, TargetStatus::Finished)); assert!(matches!(target.status, TargetStatus::Finished));
@@ -4531,7 +4990,7 @@ mod tests {
target.final_reason.clone() target.final_reason.clone()
}; };
drive_open_attempts(&config, &socket, &shared, deadline); drive_open_attempts(&config, &socket, &shared, drain_deadline);
assert_eq!( assert_eq!(
target.lock().expect("target mutex poisoned").final_reason, target.lock().expect("target mutex poisoned").final_reason,
reason reason
@@ -4955,6 +5414,37 @@ mod tests {
assert!(!cancellation.is_cancelled()); assert!(!cancellation.is_cancelled());
} }
#[test]
fn scheduler_wakeup_uses_receiver_drain_deadline_and_suppresses_due_pacing() {
let now = Instant::now();
let mut drain = ReceiverDrainState::default();
drain.request_or_join(now).unwrap();
let drain_deadline = drain.deadline().unwrap();
assert_eq!(
next_scheduler_deadline(None, Some(now), None, drain),
Some(drain_deadline)
);
let open_deadline = now + Duration::from_millis(10);
let target_drain_deadline = now + Duration::from_millis(20);
assert_eq!(
next_scheduler_deadline(
Some(open_deadline),
Some(now),
Some(target_drain_deadline),
drain,
),
Some(open_deadline)
);
drain.complete_if_expired(drain_deadline);
assert_eq!(
next_scheduler_deadline(None, Some(now), None, drain),
Some(now)
);
}
#[test] #[test]
fn staggered_pacing_assigns_distinct_target_slots() { fn staggered_pacing_assigns_distinct_target_slots() {
let interval = Duration::from_millis(80); let interval = Duration::from_millis(80);