fix(managed): skip missed probe slots

Advance delayed managed schedules to the latest due cadence slot and then directly to the first future deadline. Apply the same absolute-cadence behavior to the single runner and group burst/staggered pacing, while leaving the manual Client send API caller-paced.

Bound receive waits by the next send deadline so 10 ms intervals are not hidden behind the managed socket timeout. Add deterministic delayed-slot, finite-end, burst, staggered, and receive-bound regressions.
This commit is contained in:
Nils Andreas Svee committed 2026-07-28 01:04:43 +07:00
1 parent 5381cfc992
commit 41cd2a9589
5 files changed
+365 -54

No files matched your search

+81 -1
View File
@@ -163,6 +163,29 @@ impl Client {
/// Returns an empty event list when the socket read would block or times
/// out. Malformed or unrelated datagrams are reported as warning events.
pub fn recv_once(&mut self) -> Result<Vec<ClientEvent>, ClientError> {
self.socket
.set_read_timeout(self.runtime.config().socket_config.recv_timeout)?;
self.recv_once_inner()
}
pub(crate) fn recv_once_until(
&mut self,
deadline: Option<Instant>,
max_wait: Duration,
) -> Result<Vec<ClientEvent>, ClientError> {
let Some(timeout) = bounded_receive_timeout(
deadline,
self.runtime.config().socket_config.recv_timeout,
max_wait,
Instant::now(),
) else {
return Ok(Vec::new());
};
self.socket.set_read_timeout(Some(timeout))?;
self.recv_once_inner()
}
fn recv_once_inner(&mut self) -> Result<Vec<ClientEvent>, ClientError> {
let datagram = match recv_datagram(&self.socket, &mut self.recv_buffer) {
Ok(datagram) => datagram,
Err(err)
@@ -186,9 +209,11 @@ impl Client {
/// Receive and classify datagrams until a receive produces no events or the
/// receive budget is exhausted.
pub fn recv_available(&mut self, budget: RecvBudget) -> Result<Vec<ClientEvent>, ClientError> {
self.socket
.set_read_timeout(self.runtime.config().socket_config.recv_timeout)?;
let mut all_events = Vec::new();
for _ in 0..budget.max_packets {
let events = self.recv_once()?;
let events = self.recv_once_inner()?;
if events.is_empty() {
break;
}
@@ -243,6 +268,13 @@ impl Client {
self.runtime.packets_sent()
}
pub(crate) fn send_managed_probe(
&mut self,
scheduled_at: Instant,
) -> Result<Vec<ClientEvent>, ClientError> {
self.send_managed_probe_inner(scheduled_at, None)
}
fn send_probe_inner(
&mut self,
override_ts: Option<ClientTimestamp>,
@@ -260,6 +292,26 @@ impl Client {
})
})
}
fn send_managed_probe_inner(
&mut self,
scheduled_at: Instant,
override_ts: Option<ClientTimestamp>,
) -> Result<Vec<ClientEvent>, ClientError> {
let socket = &self.socket;
self.runtime
.send_probe_for_deadline(override_ts, scheduled_at, |packet| {
let sent_at = override_ts.unwrap_or_else(ClientTimestamp::now);
let send_call_start = Instant::now();
let bytes = socket.send(packet)?;
let send_call = send_call_start.elapsed();
Ok(SendProbeResult {
sent_at,
bytes,
send_call,
})
})
}
}
#[cfg(test)]
@@ -267,6 +319,34 @@ impl Client {
fn send_probe_at(&mut self, ts: ClientTimestamp) -> Result<Vec<ClientEvent>, ClientError> {
self.send_probe_inner(Some(ts))
}
fn send_managed_probe_at(
&mut self,
scheduled_at: Instant,
ts: ClientTimestamp,
) -> Result<Vec<ClientEvent>, ClientError> {
self.send_managed_probe_inner(scheduled_at, Some(ts))
}
}
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)]