Commit Graph
155 Commits
Author SHA1 Message Date
Nils Andreas Svee 75aaf68fea Merge pull request #122 from Lochnair/fix/table-target-width
fix(cli): bound target labels in table output
2026-08-20 10:48:35 +07:00
Nils Andreas Svee 5c7d75e39e fix(cli): bound target labels in table output
TABLE output formatted every column value with format!("{value:<width$}"),
which only sets a minimum width. Target is free-form/user-controlled and
is the first default table column, so a long target label pushed every
later column out of alignment while numeric/measurement columns stayed
correct.

Add a table-only truncation for Column::Target: labels within
table_width() (18) render unchanged; longer labels keep a
character-boundary-safe prefix and end with "..." so the rendered cell
is exactly 18 characters. This is scoped to format_table_cell's Target
case only, so it cannot silently shorten any measurement/data column.
CSV, TSV, and JSONL are untouched and keep emitting the full,
untruncated target label.
2026-08-20 10:42:48 +07:00
Nils Andreas Svee 6a4dce68b3 fix(cli): preserve target order in summaries
Final multi-target summaries were printed by iterating the label-keyed
BTreeMap directly, so they came out in lexicographic label order
instead of the order targets were given on the command line. Iterate
setup.targets (the canonical prepared CLI order) instead, looking up
each target's stats by label, and keep the BTreeMap only for
event-time lookup.
2026-08-20 10:42:31 +07:00
Nils Andreas Svee d66b5ed8de chore(release): move to independent per-crate versioning at 0.5.0
Each workspace crate now owns its own version instead of the previous
lockstep scheme; irtt-proto, irtt-client, irtt-server, irtt-stats, and
irtt-cli all become 0.5.0 as the first independently-versioned release.
Workspace dependency requirements are bumped to match.

Add a root CHANGELOG.md with a real per-crate 0.5.0 section drawn from
archaeology of every merge since v0.4.0, not just path-filtered commits.

Move cargo-dist to a crate-qualified `irtt-cli/vX.Y.Z` tag namespace so
only irtt-cli (the one dist-able application) triggers a GitHub Release;
library crate tags (irtt-proto/irtt-client/irtt-server/irtt-stats) exist
purely to pin the source for a crates.io release and no longer risk
tripping the release workflow. Regenerate the renamed
irtt-cli-release.yml via `dist generate` and update the few other files
that referenced the old release.yml path by name. Also fix a stale
`features = ["full"]` in dist-workspace.toml left over from a feature-flag
refactor that dropped the "full" feature entirely, which was silently
breaking every dist build.

Rescope semver.yml's tag trigger to the same five crate-qualified tags
so semver checks keep running under the new convention.
2026-08-17 09:29:25 +07:00
Nils Andreas Svee a598dbdc5c fix(io): address review feedback on interrupted-receive retries
- Error-queue drain: an interrupted MSG_ERRQUEUE read no longer ends
  the bounded drain early as if the queue were empty (which could
  strand a still-queued TX timestamp past the point its probe is
  looked up and removed). It now reports ErrorQueueRecord::Ignored,
  consuming one attempt of the drain's existing per-call work budget
  and retrying the same slot instead.
- Fake-server recv_from test helpers (client_multi_target.rs and
  tests/support/mod.rs) now keep one absolute deadline across
  interruptions, re-applying only the remaining time on each retry
  and restoring the configured timeout afterward, mirroring the
  production blocking-receive fix these tests exist to guard rather
  than restarting the full timeout on every interruption.
2026-08-17 00:32:37 +07:00
Nils Andreas Svee ca2389174f fix(io): retry interrupted receives
Real signal delivery on Linux (observed via x86_64-musl workspace
testing) can interrupt blocking and nonblocking recv/recvmsg calls
with EINTR. Several production and test receive paths treated that as
a fatal error rather than a transient interruption:

- Client::open's blocking receive loop and Client::recv_once/
  recv_available's blocking receive returned ClientError::Socket on
  Interrupted instead of retrying within their existing timeout
  budget.
- AsyncClient::poll_open and poll_recv's nonblocking recvmsg path
  (Linux ancillary) failed the same way instead of retrying through
  the existing readiness/work-budget machinery.
- The server's ancillary receive loop could terminate the listener on
  an interrupted nonblocking recvmsg.
- The client's MSG_ERRQUEUE TX-timestamp drain turned an interrupted
  read of optional metadata into a fatal ClientError::Socket.
- Fake-server test helpers panicked or misclassified EINTR as a
  timeout instead of retrying it.

Interrupted is now retried transparently everywhere a receive can
observe it, while preserving each path's existing timing contract:

- Blocking Open reuses its already-absolute per-attempt deadline, so
  interruptions resume within the same attempt without retransmitting
  or extending the deadline.
- recv_once/recv_available compute one absolute deadline per logical
  receive (when a timeout is configured) so repeated interruptions
  cannot extend it past the configured timeout, and restore the
  socket's configured read timeout afterward so a later call in the
  same recv_available budget is unaffected.
- AsyncClient's poll_open already bounds its retry loop with
  OPEN_POLL_WORK_BUDGET; poll_recv gains an equivalent
  RECV_POLL_WORK_BUDGET, since unlike WouldBlock, Interrupted does not
  clear Tokio's readiness state, so an unbounded retry loop was
  otherwise possible under sustained signal delivery.
- The server's receive loop retries Interrupted the same way it
  already retries WouldBlock, yielding to the runtime after a bounded
  number of consecutive interruptions for the same reason.
- The error-queue drain treats an interrupted read as no record
  obtained this drain (deferring to the next natural drain point)
  rather than a socket error, matching its existing EWOULDBLOCK
  handling.

Deterministic regression tests use a small test-only injection hook
(mirroring existing send-injection test hooks) rather than real signal
timing, covering: Open completing despite interruptions with no
retransmit, Open's deadline not extending under repeated
interruptions, recv_once classifying a reply normally despite
interruptions, recv_once's timeout not extending under repeated
interruptions, and the AsyncClient equivalents for open and recv.

No protocol, session, or timeout-classification behavior changes; no
new dependencies; send-side EINTR handling is unchanged and out of
scope.
2026-08-17 00:23:24 +07:00
Nils Andreas Svee a03004b22f refactor(cli): unify target argument syntax 2026-08-15 20:56:12 +07:00
Nils Andreas Svee e4e6d63ef1 fix(cli): preserve target identity in default output 2026-08-15 20:21:04 +07:00
Nils Andreas Svee db9bd515bb refactor(stats): retain exact finite timing samples
`SampleMode::Exact` retained exact samples for eight of the eleven public
timing metrics. Send call, timer error, and server processing always built
their `TimeMetric` with retention disabled, so a finite run reported no
median for them even though they are the same public `TimeStats` type and
appear in the same summary table with a Median column.

Derive one `retain_exact` decision from the sample mode in `CoreStats::new`
and apply it to every timing metric, so exact mode means the same thing for
all of them. Running-only mode is unchanged and still retains none of them.

The retention estimator counted eight exact sample streams per measurable
reply. Rename it to the eleven exact samples an ordinary successful probe
can retain and document the wider optional-measurement upper bound, which
raises the finite estimate on this target from 668 to 764 bytes per probe.
A test counts the metrics a fully measured probe actually retains and
compares it against that constant, so retention and its estimate cannot
drift apart unnoticed.

No new median-capability type was needed: `TimeStats::median_ns` already
distinguishes a retained median from an absent one, and an optional
measurement the server never supplied still produces no samples at all.
2026-08-15 10:41:17 +07:00
Nils Andreas Svee b93e6d0ab7 refactor(stats): own retention memory estimates 2026-08-15 10:25:04 +07:00
Nils Andreas Svee 50e4633c7a refactor(cli): centralize managed target preparation
Both client applets duplicated the step from parsed arguments to a runnable
managed setup: target specs, managed target preparation, a ClientConfig
built from an arbitrary primary target, pacing conversion, and an identical
ManagedClientConfig skeleton with its own copy of the event-capacity
constant. The primary-target step was the worst of it, since the managed
driver replaces server_addr per target, so the address the applets went out
of their way to find was immediately discarded.

shared::client::prepare now performs that step once: it validates the target
set first, then returns the validated targets, one target-free ClientConfig
template, and the pacing, with managed_config()/managed_targets() for the
driver. CommonClientArgs::to_client_config no longer takes a server address.

Applet-specific semantics are unchanged: each command keeps its own duration
default and help text, the client still accepts no target for
--list-columns, and the TUI still rejects an empty target list at parse
time.
2026-08-15 10:04:08 +07:00
Nils Andreas Svee c2666d776e Merge pull request #93 from Lochnair/test/tui-rendering
test(tui): add off-screen render coverage
2026-08-15 09:55:09 +07:00
Nils Andreas Svee ed8d212a17 Merge pull request #92 from Lochnair/refactor/stats-late-reply-policy
refactor(stats): make late-reply sampling explicit
2026-08-15 09:50:01 +07:00
Nils Andreas Svee 61fdff5c98 Merge pull request #91 from Lochnair/refactor/cli-scalar-formatting
refactor(cli): unify scalar presentation formatting
2026-08-15 09:49:50 +07:00
Nils Andreas Svee 17ade8e6cb test(tui): scope render assertions to their owning panels
The frame echoes most panel content elsewhere - the session token appears in
recent events, a replying target names a chart series, and the sample
panel's last-warning row satisfies a loose warning search - so whole-frame
assertions kept claiming coverage they did not have.

Panels are now addressed structurally: has_panel matches a title in a top
border, panel_rows slices a panel's interior to its own columns, and
assertions run against the owning panel. That fixes the session token,
recent-events warning line, graph-view panel exclusions and graph-view
target rows, and converts the remaining whole-frame checks in the module so
the class of defect does not come back.

Each tightened assertion was confirmed to fail against a deliberate
regression of the behavior it protects.
2026-08-15 02:12:23 +07:00
Nils Andreas Svee c12617a669 test(tui): tighten render assertions to the panels they claim
Four assertions did not protect what they described. Target labels were
searched across the whole frame, where the first target also appears in the
session header and a replying target appears in recent events and the graph
legend; labels are now matched against the target table's own columns, with
the status column and the replying target's last sample. The sample panel's
effective RTT is now asserted as a whole row, since the fixture's adjusted
and effective values coincide and a bare "2.5ms" still matched with the
effective field dropped. The minimum-height case now rejects dashboard
content like the minimum-width case does, and the compact layout checks for
the timing panel title rather than one incidental run of spaces from the
large timing formatter.

Each tightened assertion was confirmed to fail against a deliberate
regression of the behavior it protects.
2026-08-15 02:01:18 +07:00
Nils Andreas Svee 2062535c20 test(tui): assert the warning on its sample-panel row
The whole-frame substring assertions passed as long as the warning appeared
anywhere, so a sample panel regressing to "last warning: -" would still have
been reported as covered. The assertion now locates the last-warning row and
requires it to carry the warning itself.
2026-08-15 01:49:23 +07:00
Nils Andreas Svee 17d3fed7c5 test(tui): add off-screen render coverage
The TUI had pure state and graph-viewport tests but nothing that rendered a
frame, so layout selection and panel presence were unprotected.

Eleven tests drive draw_dashboard through Ratatui's TestBackend across the
durable layouts: below the minimum width and height, the smallest usable
terminal, compact, large, multi-target, and the graph view in both target
modes, including active sample data, warning and error presentation, and
status-line state. Assertions are on visible text and layout facts rather
than full-screen snapshots.

Test-only change; production rendering is untouched.
2026-08-15 01:24:16 +07:00
Nils Andreas Svee e0dec9ac5f refactor(stats): make late-reply sampling explicit
The TUI suppressed late-reply timing by rebuilding the event with sent_at,
rtt, server_timing, one_way and received_stats erased. That leaked stats
policy into event construction and, as a side effect, reclassified a matched
late reply as an untracked late reply.

StatsConfig now carries an explicit LateReplyMode. Measure keeps today's
behavior for every existing caller. CountOnly keeps the reply a late unique
reply and keeps packet, byte and late counters updating, while suppressing
timing, one-way, server-processing and IPDV contributions and reporting
contributed_sample == false. Cumulative and rolling snapshots share the
policy. Genuinely untracked late replies stay untracked in both modes.

The TUI selects CountOnly and passes the original event straight to the
collector.
2026-08-15 01:17:46 +07:00
Nils Andreas Svee 69b90dd64a refactor(cli): unify scalar presentation formatting
Stream output, the run summary, and the TUI each carried their own copy of
the same scalar vocabulary, and they had drifted: the TUI spelled
microseconds "us" while the others used "µs", percentages guarded
non-finite values in one place but not another, and the absent-value
placeholder was spelled inline everywhere.

cmd::format now owns nanosecond-scalar, percentage, and count formatting
with one documented unit-selection and rounding policy, µs everywhere, and
signed values preserved. The frontends keep their own metric sets and
layouts; the TUI keeps its coarse wall-clock span policy, now named
format_span and documented as deliberately distinct, and uses the shared
scalar vocabulary below one second.
2026-08-15 01:11:33 +07:00
Nils Andreas Svee e3003c348f refactor(client): make managed failure classification exhaustive
classify_client_error now matches every ClientError variant explicitly, so a
new variant fails to compile here until its real classification is chosen.
NoTokioRuntime classifies as InvalidState and DatagramLengthMismatch as
Socket; ManagedTargetFailureKind::Other survives only as a deliberate
category with no default path into it.

ManagedTargetFailurePhase and ManagedTargetFailureKind gain controlled
Display impls, and the CLI client/TUI failure diagnostics use them instead
of leaking Debug enum spelling into user-facing text.
2026-08-15 01:03:35 +07:00
Nils Andreas Svee 90c04b6569 refactor(cli): simplify feature flags and make stats mandatory for clients
Remove the optional "stats" feature by merging it into the "client" and "tui" features to reduce conditional compilation complexity. Replace the "client-runtime" internal feature with direct dependency sets.
2026-08-15 00:24:28 +07:00
Nils Andreas Svee 88419d4144 build(cli): enable ancillary feature for client-runtime
Add the ancillary feature dependency to client-runtime to support advanced socket operations and metadata handling.
2026-08-14 22:23:33 +07:00
Nils Andreas Svee 446028671b test(cli): use real server for compliant peers 2026-08-14 17:21:22 +07:00
Nils Andreas Svee 2171d02203 feat(cli): route server through server set
`--bind` becomes repeatable, so one process can serve several addresses:
an explicit IPv4 and IPv6 pair on one port, or two interface addresses
that must stay distinct.

The applet now builds a `ServerSet` for every invocation, including a
single bind. There is deliberately no bind-count branch and no direct
path from the CLI to `Server`: one orchestration path means startup
errors, shutdown and listener-failure handling are identical whatever was
configured, and the single-listener case — the common one — keeps that
path exercised in normal use rather than only in tests.

The configuration stays one `ServerConfig`; addresses never enter it, and
`ServerSet` clones it into each listener. Bounds in it are therefore per
listener, which `--max-sessions` help now says. Listener lines are
printed only once the whole set is up, so a set whose second bind fails
no longer announces the first as listening.

The shutdown flag, its polling future and the current-thread runtime are
unchanged: exactly one shutdown future is created and the set fans it out
internally.
2026-08-14 16:34:02 +07:00
Nils Andreas Svee 8607720219 docs(server): describe single allowance as one instant 2026-08-14 15:00:38 +07:00
Nils Andreas Svee 5be224d1b2 feat(cli): expose server negotiation policy 2026-08-14 14:30:28 +07:00
Nils Andreas Svee 3c554a78d7 test(server): verify wildcard source selection 2026-08-14 00:30:02 +07:00
Nils Andreas Svee 565d7fb163 fix(cli): ignore the executable suffix in argv0 dispatch 2026-08-13 19:17:20 +07:00
Nils Andreas Svee a672f78e86 feat(cli): add server applet 2026-08-13 19:01:39 +07:00
Nils Andreas Svee 8651e93351 fix(tui): bound input event draining
handle_input() previously drained terminal events in an unbounded
while-poll loop, so a continuously-ready input stream could keep the
frontend inside handle_input() indefinitely, starving managed-event
processing, completion checks, peer-close stop policy, and rendering
in the outer run_tui() loop.

Bound each call to at most INPUT_EVENT_WORK_BUDGET (128) polled
terminal events, mirroring the MANAGED_EVENT_WORK_BUDGET pattern
already used for managed event draining. All polled events count
against the budget, including ignored/unknown/release events, so a
stream of non-actionable input can't bypass the bound. Remaining
events are left queued for the next outer-loop iteration rather than
dropped. Quit/Ctrl-C now also breaks out of the drain immediately
after being handled, since there is no benefit to consuming further
buffered input before the outer loop can request managed shutdown.
2026-08-11 00:12:37 +07:00
Nils Andreas Svee 17aeaf0b9b fix(tui): allocate graph history lazily 2026-08-10 23:57:27 +07:00
Nils Andreas Svee 3df71e0b69 fix(cli): account for targets in stats memory warning 2026-08-10 23:35:16 +07:00
Nils Andreas Svee b555451448 test(cli): make integration synchronization deterministic (#61) 2026-08-10 23:21:00 +07:00
Nils Andreas Svee e5c08997ed test(cli): use protocol encoders in fake peers 2026-08-10 21:55:54 +07:00
Nils Andreas Svee f0f9277469 test: trim redundant coverage 2026-08-10 21:06:49 +07:00
renovate[bot] d4cef6c2e7 fix(deps): update rust crate clap to 4.6.6 2026-08-10 11:15:26 +07:00
Nils Andreas Svee 301542186a test(cli): bound peer-close synchronization waits 2026-08-10 11:09:00 +07:00
Nils Andreas Svee 04ea4af079 test(cli): synchronize multi-target peer close 2026-08-10 11:00:12 +07:00
Nils Andreas Svee 36eaa90e2a fix(cli): harden managed frontend cutover 2026-08-09 21:51:20 +07:00
Nils Andreas Svee c599c84715 refactor(managed): remove legacy runtime 2026-08-09 20:58:36 +07:00
Nils Andreas Svee 1a9e8f28aa refactor(cli): use unified managed frontend 2026-08-09 20:27:22 +07:00
renovate[bot] 1cb4d01746 fix(deps): update rust crate clap to 4.6.5 2026-08-04 00:06:02 +07:00
Nils Andreas Svee 7cc55bd619 fix(managed): decouple peer-close control from event delivery
Add a scheduler-owned saturating peer-close counter shared with the group session. Update it before best-effort TargetFinished publication so bounded subscriber overflow cannot hide the live shutdown condition, while retaining the existing events, bounded history, and aggregate accounting.

Make grouped CLI and TUI supervision consult the durable count on each existing activity loop and funnel both count and event observations through one-shot stop policy. Preserve interruption suppression, queued-event draining, structured failures, drop reporting, and peer-close diagnostic precedence.

Cover scheduler-only accounting, non-peer exclusions, receiver and removal races, deterministic capacity-one DropOldest eviction with an active target, frontend one-shot policy, and the existing process-level peer-close and interruption behavior.
2026-07-28 15:49:53 +07:00
Nils Andreas Svee 6af4663b6e fix(cli): drive single-target runs with ManagedClient
Replace the manual single-target ClientSession loops in the stream and TUI adapters with ManagedClient::start_with_subscription so the existing worker owns opening, deadline-aware pacing, receive handling, timeout polling, draining, and close.

Use bounded subscription waits for interruption and input polling, drain queued events before and after join, and report per-subscription drops without changing output, statistics, labels, summaries, warning, or peer-close policy. Add process and unit regressions for 10 ms cadence, interruption, final-event draining, TUI terminal state, and overflow visibility.
2026-07-28 14:42:02 +07:00
Nils Andreas Svee 080d45ca67 fix(cli): stop continuous groups on peer closure
Record a continuous peer-closed TargetFinished event as the reason for requesting group shutdown and stop each CLI/TUI group at most once. Drain the managed subscription and join before applying exit policy so queued output and target failures are preserved.

Give peer closure precedence over the cancellation caused by that internal stop while keeping explicit interruption and finite-run behavior distinct. Add bounded process regressions for partial closure, mixed failures, queued rows, and interruption plus focused TUI policy coverage.
2026-07-28 14:29:15 +07:00
Nils Andreas Svee 58d11f1dec fix(cli): fail continuous runs on peer closure
Keep PeerClosed as a distinct successful library outcome while treating it as an error at the CLI and TUI boundary for uninterrupted continuous runs.

Track aggregate peer-close outcomes independently of bounded recent history and cover finite, continuous, grouped, mixed-failure, interruption, and TUI status behavior.
2026-07-28 10:23:47 +07:00
Nils Andreas Svee 6adec06442 fix(managed): expose dropped subscription events
Track a monotonic discarded-event count per bounded subscription for all overflow policies, including after hub sealing or disconnection. Preserve nonblocking publication and existing queue policies.

Warn once in streaming CLI output and mark TUI summaries/status as incomplete when DropOldest discarded events. Add exact overflow-counter and incomplete-marker regressions.
2026-07-28 01:04:43 +07:00
Nils Andreas Svee 5381cfc992 fix(managed): bound completed target retention
Retain only the 256 most recent target outcomes in the final group snapshot while tracking total, successful, failed, and discarded outcome counts. Publish each authoritative TargetFinished event before inserting into the bounded history.

Update CLI/TUI exit decisions to use aggregate counts and add a dynamic churn regression proving every outcome remains observable while join retention stays bounded.
2026-07-28 01:04:42 +07:00
Nils Andreas Svee 8e58c0f04b fix(managed): keep empty dynamic groups idle
Track desired membership separately from active target state and make natural completion an explicit group policy. Empty authoritative updates now remove all targets without terminating the scheduler, while static CLI groups retain all-target completion.

Document that construction still requires an address-family-defining target and add regressions for empty replacement, later re-addition, idle control responsiveness, idle cancellation, and finite static completion.
2026-07-28 01:04:42 +07:00