No CLI, TUI, protocol, stats, manifest, lockfile, documentation, workflow, push, or PR changes.
## Summary
Introduces the first functional unified Tokio managed client driver.
- Adds `irtt_client::managed` as the permanent new namespace.
- Adds one authoritative `ManagedClientTask` owning zero or more per-target `AsyncClient`s.
- Supports static multi-target operation, explicit-stop empty controllers, burst/staggered pacing, graceful drain/close, per-target failure isolation, durable outcomes, and task-drop abandonment.
- Keeps the existing root managed implementation as temporary legacy scaffolding for CLI/TUI compatibility.
- Extracts shared `TargetId` so legacy and new managed APIs use the same type.
## Testing
- Adds exactly 20 focused managed lifecycle tests (626 lines).
- Passes formatting, Tokio/all-feature checks and strict Clippy.
- Passes no-default, Tokio, client all-feature, and workspace all-feature tests.
- Focused managed suite passed 10 consecutive repetitions in both the development and detached final-HEAD worktrees.
- Passes Rust 1.85 metadata, declared Rust 1.88 check, and Linux/Windows cross-target checks.
## Scope
No CLI, TUI, protocol, stats, manifest, lockfile, documentation, workflow, push, or PR changes.
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
### 💡 Codex Review
Here are some automated review suggestions for this pull request.
**Reviewed commit:** `1cb6e07706`
<details> <summary>ℹ️ About Codex in GitHub</summary>
<br/>
[Your team has set up Codex to review pull requests in this repo](https://chatgpt.com/codex/cloud/settings/general). Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
</details>
When several replies are already queued as the drain deadline expires, poll_recv processes only one datagram, after which this unconditional transition to Closing drops every remaining queued reply with the socket. High-rate or burst tests can therefore omit valid reply events and undercount replies_received, late, or duplicates even when those packets arrived within the configured drain window; keep polling receives until the socket reports pending/WouldBlock before closing at the deadline.
Useful? React with 👍 / 👎.
**<sub><sub></sub></sub> Drain all queued datagrams before closing**
When several replies are already queued as the drain deadline expires, `poll_recv` processes only one datagram, after which this unconditional transition to `Closing` drops every remaining queued reply with the socket. High-rate or burst tests can therefore omit valid reply events and undercount `replies_received`, `late`, or `duplicates` even when those packets arrived within the configured drain window; keep polling receives until the socket reports pending/WouldBlock before closing at the deadline.
Useful? React with 👍 / 👎.
When loose negotiation accepts an IntervalReduced response, this gate continues using the originally configured interval rather than the interval stored in each target's negotiated schedule. In staggered mode—even with only one target—a request for a 1-second interval negotiated down to 100 ms keeps the gate closed for roughly 1 second after each send, causing skip_missed_probe_slots_at to discard most negotiated probe slots and silently produce an incorrect experiment rate; derive the gate spacing from the active targets' negotiated intervals.
Useful? React with 👍 / 👎.
**<sub><sub></sub></sub> Pace against the negotiated probe interval**
When loose negotiation accepts an `IntervalReduced` response, this gate continues using the originally configured interval rather than the interval stored in each target's negotiated schedule. In staggered mode—even with only one target—a request for a 1-second interval negotiated down to 100 ms keeps the gate closed for roughly 1 second after each send, causing `skip_missed_probe_slots_at` to discard most negotiated probe slots and silently produce an incorrect experiment rate; derive the gate spacing from the active targets' negotiated intervals.
Useful? React with 👍 / 👎.
Preserve the primary outcome when drain receives fail
Once a target enters Draining, primary_end already records a successful test completion or requested stop, but a socket error surfaced by poll_recv here replaces that result with Failed(Receiving). For example, a connected UDP socket can report a late ICMP error while performing this optional cleanup, making an otherwise completed experiment increment failed_target_outcomes; preserve primary_end and record this as cleanup_failure, as the subsequent closing state already does for close errors.
Useful? React with 👍 / 👎.
**<sub><sub></sub></sub> Preserve the primary outcome when drain receives fail**
Once a target enters `Draining`, `primary_end` already records a successful test completion or requested stop, but a socket error surfaced by `poll_recv` here replaces that result with `Failed(Receiving)`. For example, a connected UDP socket can report a late ICMP error while performing this optional cleanup, making an otherwise completed experiment increment `failed_target_outcomes`; preserve `primary_end` and record this as `cleanup_failure`, as the subsequent closing state already does for close errors.
Useful? React with 👍 / 👎.
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
### 💡 Codex Review
Here are some automated review suggestions for this pull request.
**Reviewed commit:** `1af8afcfae`
<details> <summary>ℹ️ About Codex in GitHub</summary>
<br/>
[Your team has set up Codex to review pull requests in this repo](https://chatgpt.com/codex/cloud/settings/general). Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
</details>
Reject unschedulable final-drain durations at construction
When final_drain is large enough that latest.checked_add(final_drain) overflows (for example, Duration::MAX), construction accepts the configuration but this path replaces the already-determined TestComplete or Stopped result with a target failure. That incorrectly increments failed_target_outcomes after an otherwise successful experiment; validate that the drain duration is schedulable in ManagedClient::task, or preserve primary_end and report the overflow as a cleanup failure.
Useful? React with 👍 / 👎.
**<sub><sub></sub></sub> Reject unschedulable final-drain durations at construction**
When `final_drain` is large enough that `latest.checked_add(final_drain)` overflows (for example, `Duration::MAX`), construction accepts the configuration but this path replaces the already-determined `TestComplete` or `Stopped` result with a target failure. That incorrectly increments `failed_target_outcomes` after an otherwise successful experiment; validate that the drain duration is schedulable in `ManagedClient::task`, or preserve `primary_end` and report the overflow as a cleanup failure.
Useful? React with 👍 / 👎.
When graceful stop is requested after an open request has been submitted, this branch immediately drops both the client and AsyncOpenState. If the server has already allocated a session—or poll_open has retained a cleanup-close packet after rejecting a trusted reply—the server never receives a close and must retain that session until its own timeout. Continue driving the open transaction during stop so retained cleanup completes, and close normally if opening succeeds.
Useful? React with 👍 / 👎.
**<sub><sub></sub></sub> Finish in-flight opening cleanup before stopping**
When graceful stop is requested after an open request has been submitted, this branch immediately drops both the client and `AsyncOpenState`. If the server has already allocated a session—or `poll_open` has retained a cleanup-close packet after rejecting a trusted reply—the server never receives a close and must retain that session until its own timeout. Continue driving the open transaction during stop so retained cleanup completes, and close normally if opening succeeds.
Useful? React with 👍 / 👎.
Reset the stagger gate when a target becomes active
When this target opens while another target's existing stagger gate is still active, the gate retains the spacing computed for the old active set. For example, one 1-second target can send while it is the sole active target and set a 1-second gate; a second target that opens milliseconds later cannot send its initial probe until that gate expires, so a sub-second run sends no probes at all and longer runs skip multiple slots. Reset or clamp send_gate using the newly active membership when installing this state.
Useful? React with 👍 / 👎.
**<sub><sub></sub></sub> Reset the stagger gate when a target becomes active**
When this target opens while another target's existing stagger gate is still active, the gate retains the spacing computed for the old active set. For example, one 1-second target can send while it is the sole active target and set a 1-second gate; a second target that opens milliseconds later cannot send its initial probe until that gate expires, so a sub-second run sends no probes at all and longer runs skip multiple slots. Reset or clamp `send_gate` using the newly active membership when installing this state.
Useful? React with 👍 / 👎.
Expire overdue probes before matching queued replies
When the Tokio task is not polled across a probe timeout and a reply arrives during that stall, both the socket and timeout are ready on the next poll, but this receive runs before the overdue poll_timeouts_at call below. The reply is therefore removed from pending and reported as an on-time EchoReply, suppressing both the expected EchoLoss and LateReply and corrupting loss/latency results under executor load. Process deadlines at now before receiving queued datagrams.
Useful? React with 👍 / 👎.
**<sub><sub></sub></sub> Expire overdue probes before matching queued replies**
When the Tokio task is not polled across a probe timeout and a reply arrives during that stall, both the socket and timeout are ready on the next poll, but this receive runs before the overdue `poll_timeouts_at` call below. The reply is therefore removed from `pending` and reported as an on-time `EchoReply`, suppressing both the expected `EchoLoss` and `LateReply` and corrupting loss/latency results under executor load. Process deadlines at `now` before receiving queued datagrams.
Useful? React with 👍 / 👎.
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
### 💡 Codex Review
Here are some automated review suggestions for this pull request.
**Reviewed commit:** `8445c0cd99`
<details> <summary>ℹ️ About Codex in GitHub</summary>
<br/>
[Your team has set up Codex to review pull requests in this repo](https://chatgpt.com/codex/cloud/settings/general). Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
</details>
Avoid lengthening the gate when an active target exits
When a target leaves the active set, this recomputation can replace an expired short gate with last_stagger_send + the remaining target's much longer interval. For example, if a 100 ms target finishes after its last send at 900 ms while a later-opened 1-second target is due at 1.05 seconds, removing the first target moves the gate to 1.9 seconds; the remaining target then misses its due probe entirely if its negotiated run ends before that gate. Preserve the existing gate on removals, or otherwise ensure membership changes cannot postpone a target beyond its own next-send deadline.
Useful? React with 👍 / 👎.
**<sub><sub></sub></sub> Avoid lengthening the gate when an active target exits**
When a target leaves the active set, this recomputation can replace an expired short gate with `last_stagger_send +` the remaining target's much longer interval. For example, if a 100 ms target finishes after its last send at 900 ms while a later-opened 1-second target is due at 1.05 seconds, removing the first target moves the gate to 1.9 seconds; the remaining target then misses its due probe entirely if its negotiated run ends before that gate. Preserve the existing gate on removals, or otherwise ensure membership changes cannot postpone a target beyond its own next-send deadline.
Useful? React with 👍 / 👎.
Exclude already-replied probes from the drain deadline
Because latest_committed_timeout records every accepted probe even after its reply removed it from pending, a successful finite run waits until that obsolete timeout plus final_drain before closing. With the defaults, an immediate reply to the last probe sent around 2 seconds leaves a timeout around 6 seconds, so a nominal 3-second test does not return until roughly 6.1 seconds; larger configured probe timeouts make the delay arbitrarily worse. Base this deadline on currently pending or timed-out probes rather than the historical last committed timeout.
Useful? React with 👍 / 👎.
**<sub><sub></sub></sub> Exclude already-replied probes from the drain deadline**
Because `latest_committed_timeout` records every accepted probe even after its reply removed it from `pending`, a successful finite run waits until that obsolete timeout plus `final_drain` before closing. With the defaults, an immediate reply to the last probe sent around 2 seconds leaves a timeout around 6 seconds, so a nominal 3-second test does not return until roughly 6.1 seconds; larger configured probe timeouts make the delay arbitrarily worse. Base this deadline on currently pending or timed-out probes rather than the historical last committed timeout.
Useful? React with 👍 / 👎.
When an open target encounters a send-side error, this branch immediately marks it terminal and drops the AsyncClient without sending its prepared close packet. This occurs through ordinary configuration rather than only transport failure—for example, with max_pending_probes = 1 and an interval shorter than probe_timeout, the second send returns PendingLimitExceeded while the first probe remains outstanding—so the server retains an allocated session until its own timeout and the outstanding probe is never drained. Preserve the classified failure as the primary end reason while driving the target through drain/close cleanup.
Useful? React with 👍 / 👎.
**<sub><sub></sub></sub> Close active sessions after send failures**
When an open target encounters a send-side error, this branch immediately marks it terminal and drops the `AsyncClient` without sending its prepared close packet. This occurs through ordinary configuration rather than only transport failure—for example, with `max_pending_probes = 1` and an interval shorter than `probe_timeout`, the second send returns `PendingLimitExceeded` while the first probe remains outstanding—so the server retains an allocated session until its own timeout and the outstanding probe is never drained. Preserve the classified failure as the primary end reason while driving the target through drain/close cleanup.
Useful? React with 👍 / 👎.
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.
You have reached your Codex usage limits for code reviews. You can see your limits in the [Codex usage dashboard](https://chatgpt.com/codex/cloud/settings/usage).
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your [settings](https://chatgpt.com/codex/cloud/settings/code-review).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Introduces the first functional unified Tokio managed client driver.
irtt_client::managedas the permanent new namespace.ManagedClientTaskowning zero or more per-targetAsyncClients.TargetIdso legacy and new managed APIs use the same type.Testing
Scope
No CLI, TUI, protocol, stats, manifest, lockfile, documentation, workflow, push, or PR changes.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:
1cb6e07706ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
@@ -0,0 +1,1561 @@use std::{When several replies are already queued as the drain deadline expires,
poll_recvprocesses only one datagram, after which this unconditional transition toClosingdrops every remaining queued reply with the socket. High-rate or burst tests can therefore omit valid reply events and undercountreplies_received,late, orduplicateseven when those packets arrived within the configured drain window; keep polling receives until the socket reports pending/WouldBlock before closing at the deadline.Useful? React with 👍 / 👎.
When loose negotiation accepts an
IntervalReducedresponse, this gate continues using the originally configured interval rather than the interval stored in each target's negotiated schedule. In staggered mode—even with only one target—a request for a 1-second interval negotiated down to 100 ms keeps the gate closed for roughly 1 second after each send, causingskip_missed_probe_slots_atto discard most negotiated probe slots and silently produce an incorrect experiment rate; derive the gate spacing from the active targets' negotiated intervals.Useful? React with 👍 / 👎.
Once a target enters
Draining,primary_endalready records a successful test completion or requested stop, but a socket error surfaced bypoll_recvhere replaces that result withFailed(Receiving). For example, a connected UDP socket can report a late ICMP error while performing this optional cleanup, making an otherwise completed experiment incrementfailed_target_outcomes; preserveprimary_endand record this ascleanup_failure, as the subsequent closing state already does for close errors.Useful? React with 👍 / 👎.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:
1af8afcfaeℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
@@ -0,0 +1,1561 @@use std::{When
final_drainis large enough thatlatest.checked_add(final_drain)overflows (for example,Duration::MAX), construction accepts the configuration but this path replaces the already-determinedTestCompleteorStoppedresult with a target failure. That incorrectly incrementsfailed_target_outcomesafter an otherwise successful experiment; validate that the drain duration is schedulable inManagedClient::task, or preserveprimary_endand report the overflow as a cleanup failure.Useful? React with 👍 / 👎.
When graceful stop is requested after an open request has been submitted, this branch immediately drops both the client and
AsyncOpenState. If the server has already allocated a session—orpoll_openhas retained a cleanup-close packet after rejecting a trusted reply—the server never receives a close and must retain that session until its own timeout. Continue driving the open transaction during stop so retained cleanup completes, and close normally if opening succeeds.Useful? React with 👍 / 👎.
When this target opens while another target's existing stagger gate is still active, the gate retains the spacing computed for the old active set. For example, one 1-second target can send while it is the sole active target and set a 1-second gate; a second target that opens milliseconds later cannot send its initial probe until that gate expires, so a sub-second run sends no probes at all and longer runs skip multiple slots. Reset or clamp
send_gateusing the newly active membership when installing this state.Useful? React with 👍 / 👎.
@@ -0,0 +671,4 @@}}}let received = match client.poll_recv(cx) {When the Tokio task is not polled across a probe timeout and a reply arrives during that stall, both the socket and timeout are ready on the next poll, but this receive runs before the overdue
poll_timeouts_atcall below. The reply is therefore removed frompendingand reported as an on-timeEchoReply, suppressing both the expectedEchoLossandLateReplyand corrupting loss/latency results under executor load. Process deadlines atnowbefore receiving queued datagrams.Useful? React with 👍 / 👎.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:
8445c0cd99ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
@@ -0,0 +1,1561 @@use std::{When a target leaves the active set, this recomputation can replace an expired short gate with
last_stagger_send +the remaining target's much longer interval. For example, if a 100 ms target finishes after its last send at 900 ms while a later-opened 1-second target is due at 1.05 seconds, removing the first target moves the gate to 1.9 seconds; the remaining target then misses its due probe entirely if its negotiated run ends before that gate. Preserve the existing gate on removals, or otherwise ensure membership changes cannot postpone a target beyond its own next-send deadline.Useful? React with 👍 / 👎.
Because
latest_committed_timeoutrecords every accepted probe even after its reply removed it frompending, a successful finite run waits until that obsolete timeout plusfinal_drainbefore closing. With the defaults, an immediate reply to the last probe sent around 2 seconds leaves a timeout around 6 seconds, so a nominal 3-second test does not return until roughly 6.1 seconds; larger configured probe timeouts make the delay arbitrarily worse. Base this deadline on currently pending or timed-out probes rather than the historical last committed timeout.Useful? React with 👍 / 👎.
@@ -0,0 +1099,4 @@now,OpenSessionFailureCleanup::Drain,);SendResult::Failed { accepted }When an open target encounters a send-side error, this branch immediately marks it terminal and drops the
AsyncClientwithout sending its prepared close packet. This occurs through ordinary configuration rather than only transport failure—for example, withmax_pending_probes = 1and an interval shorter thanprobe_timeout, the second send returnsPendingLimitExceededwhile the first probe remains outstanding—so the server retains an allocated session until its own timeout and the outstanding probe is never drained. Preserve the classified failure as the primary end reason while driving the target through drain/close cleanup.Useful? React with 👍 / 👎.
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.