Compare commits

..
2 Commits
Author SHA1 Message Date
Nils Andreas Svee 9c703ecff2 Merge branch 'fix/managed-finite-lifetime-wake'
Rust CI / cargo fmt (push) Successful in 19s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Successful in 35s
Rust CI / cargo clippy (push) Successful in 37s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Failing after 7s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Failing after 8s
Rust CI / cargo doc (push) Successful in 23s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Successful in 1m16s
Fuzz Smoke / cargo fuzz smoke (push) Failing after 27s
Supply chain / cargo-deny advisories (push) Successful in 9s
Supply chain / actionlint (push) Successful in 3s
Supply chain / cargo-deny bans licenses sources (push) Successful in 8s
Netem Interop / netem interop (push) Successful in 1m3s
2026-10-04 15:47:51 +07:00
Nils Andreas Svee 798efaf360 fix(client): wake finite managed schedules under backpressure
Expose the negotiated schedule end only while the schedule is unfinished,
and include it among Active targets' non-send deadlines. This keeps finite
expiry independent of socket writability and of Burst/Staggered pacing.
Staggered release gating continues to apply only to send opportunities.

The existing expiry path marks the schedule finished, removing its lifetime
deadline before pending probes continue draining. No new state, readiness
handling, cadence, or drain/close policy is introduced. Continuous schedules
have no lifetime deadline.

The prior public-API Linux namespace/veth/TBF investigation observed genuine
EAGAIN with Burst pacing, 500 ms duration, 1 ms interval, 1400-byte probes,
and 20 ms probe timeout. Polling stopped around 327 ms after pending probes
disappeared, remained stalled beyond 1.5 s, and resumed on queue release.
Staggered applicability follows from the shared deadline selection; it was
not separately reproduced under TBF.

Retain two deterministic owning-layer tests for schedule deadline lifetime
and managed selection under send_waiting and stagger gating, using ordinary
Instant values and a production socket. Keep the privileged networking
harness as investigation evidence rather than adding bespoke test machinery.

Validation: formatting; focused schedule/managed tests; managed lifecycle,
behavior, and blocking-owner integration tests; workspace all-feature check,
all-target tests, doctests, and strict Clippy; client no-default-features
check/tests/strict Clippy and Tokio-free normal dependency tree; Linux
all-feature client and integration suites, including ancillary transport.
2026-10-04 15:41:06 +07:00
2 changed files with 115 additions and 0 deletions

No files matched your search

@@ -48,6 +48,11 @@ impl ProbeSchedule {
self.next_send_at
}
/// Wake for finite expiry independently of send readiness until finished.
pub(crate) fn end_deadline(&self) -> Option<Instant> {
self.end_at.filter(|_| !self.is_finished())
}
pub(crate) fn interval(&self) -> Duration {
self.interval
}
@@ -152,3 +157,42 @@ pub(crate) fn instant_abs_diff(left: Instant, right: Instant) -> Duration {
.or_else(|| right.checked_duration_since(left))
.unwrap_or(Duration::ZERO)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lifetime_deadline_exists_only_for_an_unfinished_finite_schedule() {
let start = Instant::now();
let end = start + Duration::from_millis(500);
let mut negotiated = NegotiatedParams {
params: irtt_proto::Params {
duration_ns: 500_000_000,
interval_ns: 1_000_000,
..irtt_proto::Params::default()
},
restrictions: Vec::new(),
};
let mut finite = ProbeSchedule::new(start, &negotiated).unwrap();
assert_eq!(finite.end_deadline(), Some(end));
assert!(finite.permit_probe_at(end - Duration::from_nanos(1)));
assert_eq!(finite.end_deadline(), Some(end));
assert!(!finite.permit_probe_at(end));
assert!(finite.is_finished());
assert_eq!(finite.end_deadline(), None);
let mut finite = ProbeSchedule::new(start, &negotiated).unwrap();
let commit = finite
.preflight_managed_commit(start, end - Duration::from_nanos(1))
.unwrap();
finite.commit(commit);
assert!(finite.is_finished());
assert_eq!(finite.end_deadline(), None);
negotiated.params.duration_ns = 0;
let mut continuous = ProbeSchedule::new(start, &negotiated).unwrap();
assert!(continuous.permit_probe_at(end));
assert_eq!(continuous.end_deadline(), None);
}
}
+71
View File
@@ -1936,6 +1936,12 @@ impl ManagedClientTask {
non_send_deadline = non_send_deadline
.into_iter()
.chain(client.next_probe_timeout_deadline())
.chain(
target
.schedule
.as_ref()
.and_then(ProbeSchedule::end_deadline),
)
.min();
if target.desired && target.retirement.is_none() && !target.send_waiting {
send_deadline = send_deadline
@@ -2399,6 +2405,71 @@ fn close_timeout_failure() -> ManagedTargetFailure {
mod tests {
use super::*;
// Real EAGAIN needs privileged queue shaping. Inspect only the owning
// deadline selection here, using a production socket without sending.
#[test]
fn finite_lifetime_deadline_survives_send_waiting_and_stagger_gating() {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
runtime.block_on(async {
let start = Instant::now();
let end = start + Duration::from_millis(500);
let negotiated = crate::NegotiatedParams {
params: irtt_proto::Params {
duration_ns: 500_000_000,
interval_ns: 1_000_000,
..irtt_proto::Params::default()
},
restrictions: Vec::new(),
};
for pacing in [ManagedPacing::Burst, ManagedPacing::Staggered] {
let (mut task, _handle) = ManagedClient::task(
ManagedClientConfig {
pacing,
..ManagedClientConfig::default()
},
vec![ManagedTargetConfig::new("target", "127.0.0.1:9")],
)
.unwrap();
let client = AsyncClient::connect(ClientConfig {
server_addr: "127.0.0.1:9".into(),
..ClientConfig::default()
})
.await
.unwrap();
task.state = DriverState::Running;
task.install_target_state(0, TargetState::Active { client });
task.send_gate = Some(end + Duration::from_secs(1));
for send_waiting in [false, true] {
task.targets[0].send_waiting = send_waiting;
task.targets[0].schedule =
Some(ProbeSchedule::new(start, &negotiated).unwrap());
let expected = if pacing == ManagedPacing::Burst && !send_waiting {
start
} else {
end
};
assert_eq!(task.next_deadline(), Some(expected));
task.targets[0]
.schedule
.as_mut()
.unwrap()
.permit_probe_at(end);
assert_eq!(task.next_deadline(), None);
}
let mut continuous = negotiated.clone();
continuous.params.duration_ns = 0;
task.targets[0].schedule = Some(ProbeSchedule::new(start, &continuous).unwrap());
assert_eq!(task.next_deadline(), None);
}
});
}
#[test]
fn release_gate_only_delays_an_existing_send_deadline() {
let earlier = Instant::now();