Compare commits

...
Author SHA1 Message Date
Nils Andreas Svee cf48988170 chore(release): irtt-client 0.5.2, irtt-rs 0.7.1
Rust CI / cargo fmt (push) Successful in 14s
Rust CI / cargo clippy (push) Successful in 29s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Successful in 25s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Failing after 9s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Failing after 10s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Successful in 43s
Rust CI / cargo doc (push) Successful in 23s
Supply chain / cargo-deny advisories (push) Successful in 7s
Fuzz Smoke / cargo fuzz smoke (push) Failing after 29s
Supply chain / cargo-deny bans licenses sources (push) Successful in 6s
Supply chain / actionlint (push) Successful in 3s
Netem Interop / netem interop (push) Successful in 1m8s
Semver / cargo-semver-checks (push) Successful in 58s
Publish irtt-server image / publish (push) Successful in 1m13s
Fix kernel transmit-timestamp correlation after failed probe sends and accept ordinary boolean values for the server DSCP environment option.
2026-09-25 00:29:52 +07:00
renovate Bot 41aee2a551 fix(deps): update rust crate clap to 4.6.7
Rust CI / cargo fmt (push) Canceled after 0s
Rust CI / cargo clippy (push) Canceled after 0s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Canceled after 0s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Canceled after 0s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Canceled after 0s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Canceled after 0s
Rust CI / cargo doc (push) Canceled after 0s
Fuzz Smoke / cargo fuzz smoke (push) Canceled after 0s
Netem Interop / netem interop (push) Canceled after 0s
Supply chain / cargo-deny advisories (push) Canceled after 0s
Supply chain / cargo-deny bans licenses sources (push) Canceled after 0s
Supply chain / actionlint (push) Canceled after 0s
2026-09-24 10:21:00 +00:00
renovate Bot 722539c8d7 chore(deps): update github actions to v2
Rust CI / cargo fmt (push) Canceled after 0s
Rust CI / cargo clippy (push) Canceled after 0s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Canceled after 0s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Canceled after 0s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Canceled after 0s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Canceled after 0s
Rust CI / cargo doc (push) Canceled after 0s
Fuzz Smoke / cargo fuzz smoke (push) Canceled after 0s
Netem Interop / netem interop (push) Canceled after 0s
Supply chain / cargo-deny advisories (push) Canceled after 0s
Supply chain / cargo-deny bans licenses sources (push) Canceled after 0s
Supply chain / actionlint (push) Canceled after 0s
2026-09-24 10:20:48 +00:00
renovate Bot 8369ec3502 chore(deps): update github actions
Rust CI / cargo fmt (push) Canceled after 0s
Rust CI / cargo clippy (push) Canceled after 0s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Canceled after 0s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Canceled after 0s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Canceled after 0s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Canceled after 0s
Rust CI / cargo doc (push) Canceled after 0s
Fuzz Smoke / cargo fuzz smoke (push) Canceled after 0s
Netem Interop / netem interop (push) Canceled after 0s
Supply chain / cargo-deny advisories (push) Canceled after 0s
Supply chain / cargo-deny bans licenses sources (push) Canceled after 0s
Supply chain / actionlint (push) Canceled after 0s
2026-09-24 10:19:59 +00:00
renovate Bot 1df69969db chore(deps): maintain cargo lockfile
Rust CI / cargo fmt (push) Canceled after 0s
Rust CI / cargo clippy (push) Canceled after 0s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Canceled after 0s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Canceled after 0s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Canceled after 0s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Canceled after 0s
Rust CI / cargo doc (push) Canceled after 0s
Fuzz Smoke / cargo fuzz smoke (push) Canceled after 0s
Netem Interop / netem interop (push) Canceled after 0s
Supply chain / cargo-deny advisories (push) Canceled after 0s
Supply chain / cargo-deny bans licenses sources (push) Canceled after 0s
Supply chain / actionlint (push) Canceled after 0s
2026-09-24 10:19:40 +00:00
Nils Andreas Svee 4908921fb8 Merge branch 'docs/doctests'
Rust CI / cargo fmt (push) Canceled after 0s
Rust CI / cargo clippy (push) Canceled after 0s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Canceled after 0s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Canceled after 0s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Canceled after 0s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Canceled after 0s
Rust CI / cargo doc (push) Canceled after 0s
Fuzz Smoke / cargo fuzz smoke (push) Canceled after 0s
Netem Interop / netem interop (push) Canceled after 0s
Supply chain / cargo-deny advisories (push) Canceled after 0s
Supply chain / cargo-deny bans licenses sources (push) Canceled after 0s
Supply chain / actionlint (push) Canceled after 0s
2026-09-24 17:13:02 +07:00
Nils Andreas Svee 0d69255e9e docs(client): add executable API examples 2026-09-24 16:51:03 +07:00
Nils Andreas Svee d7d6e76682 docs(server): add executable API examples 2026-09-24 16:40:04 +07:00
Nils Andreas Svee 328ff17901 docs(irtt-proto): add runnable doctests for public API
Add executable doctests documenting the consumer-facing contract of
irtt-proto: the directional request encode/decode/verify split, Params
construction and round-trip, omitted-vs-explicit-zero presence, open and
echo reply round trips, packet length calculation, and HMAC verification.

No conventional tests are migrated: every existing irtt-proto test
exercises wire vectors, flag matrices, malformed input, layout rules,
HMAC error behavior, or encode/decode round trips, which the crate's
AGENTS.md treats as durable compatibility coverage rather than
documentation cleanup targets.
2026-09-24 16:29:40 +07:00
Nils Andreas Svee 19782dd3fd docs(irtt-stats): add runnable doctests for public API
Rust CI / cargo fmt (push) Successful in 22s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Successful in 42s
Rust CI / cargo clippy (push) Successful in 45s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Successful in 1m31s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Successful in 41s
Rust CI / cargo doc (push) Successful in 38s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Successful in 1m30s
Fuzz Smoke / cargo fuzz smoke (push) Successful in 1m44s
Supply chain / cargo-deny advisories (push) Successful in 10s
Netem Interop / netem interop (push) Successful in 1m13s
Supply chain / cargo-deny bans licenses sources (push) Successful in 6s
Supply chain / actionlint (push) Successful in 3s
Add executable doctests that document the consumer-facing contract of
irtt-stats: finite vs continuous retention, the exact-median guarantee,
the retained-memory planning estimate, and count-/time-based rolling
snapshots. Migrate the send-call exact-median test into the
StatsConfig::finite() doc, which fully subsumes it.
2026-09-18 00:10:49 +07:00
33 changed files with 800 additions and 86 deletions

No files matched your search

+4 -4
View File
@@ -28,7 +28,7 @@ jobs:
persist-credentials: false
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
with:
components: rustfmt
cache: false
@@ -48,7 +48,7 @@ jobs:
persist-credentials: false
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
with:
components: clippy
cache: false
@@ -109,7 +109,7 @@ jobs:
sudo apt-get install --no-install-recommends musl-tools
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
with:
target: ${{ matrix.target }}
cache: false
@@ -147,7 +147,7 @@ jobs:
persist-credentials: false
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
with:
cache: false
+2 -2
View File
@@ -42,7 +42,7 @@ jobs:
# (native builds, no emulation), but buildx still needs a driver that
# can assemble a multi-platform manifest list.
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4
- name: Log in to Gitea Container Registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
@@ -76,7 +76,7 @@ jobs:
# below), never a separate build, so the hash matches everywhere.
- name: Build and push to Gitea
id: build
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7
with:
context: .
platforms: linux/amd64,linux/arm64
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
persist-credentials: false
- name: Install stable Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
with:
cache: false
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
persist-credentials: false
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
with:
cache: false
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
persist-credentials: false
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
with:
cache: false
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
# It remains here while portable actionlint runs with Gitea CI, but scans
# both workflow directories through its GitHub Actions rule set.
- name: Run zizmor
uses: zizmorcore/zizmor-action@70fb788f84895a7701f5643d103d587e460b5c99 # v0.6.3
uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
with:
persona: regular
inputs: |
+1 -1
View File
@@ -56,7 +56,7 @@ jobs:
sudo apt-get install --no-install-recommends musl-tools
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
with:
target: ${{ matrix.target }}
cache-key: ${{ matrix.task }}-${{ matrix.target }}
+2 -2
View File
@@ -31,7 +31,7 @@ permissions:
jobs:
scan-scheduled:
if: ${{ github.event_name == 'push' || github.event_name == 'schedule' }}
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@6e4298ebc4db23e847df9b2e2de2939d6f066c67" # v2.5.1
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@a345acffa64b0eaede81a3d9aae6141214d9c8fc" # v2.6.0
with:
# Example of specifying custom arguments
scan-args: |-
@@ -40,7 +40,7 @@ jobs:
./
scan-pr:
if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }}
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@6e4298ebc4db23e847df9b2e2de2939d6f066c67" # v2.5.1
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@a345acffa64b0eaede81a3d9aae6141214d9c8fc" # v2.6.0
with:
# Example of specifying custom arguments
scan-args: |-
+2 -2
View File
@@ -29,7 +29,7 @@ jobs:
persist-credentials: false
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1
uses: actions-rust-lang/setup-rust-toolchain@ecabd13d1c56bd1345c230e542e9144811ad706f # v2
with:
components: clippy
cache-key: clippy-sarif
@@ -60,7 +60,7 @@ jobs:
exit 0
- name: Upload Clippy SARIF
uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4
uses: github/codeql-action/upload-sarif@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4
if: always()
with:
sarif_file: rust-clippy-results.sarif
+12
View File
@@ -28,6 +28,12 @@ release. The format is loosely based on
## irtt-client
### 0.5.2
#### Fixed
- Kernel transmit-timestamp correlation is now invalidated when a probe send fails, preventing a later timestamp from being matched to the wrong probe.
### 0.5.1
#### Added
@@ -108,6 +114,12 @@ release. The format is loosely based on
## irtt-rs
### 0.7.1
#### Fixed
- `IRTT_SERVER_NO_DSCP` now accepts ordinary boolean environment values such as `1` and `0`; falsey values leave DSCP marking enabled, while truthy values disable it. The `--no-dscp` command-line flag remains a flag without a value.
### 0.7.0
#### Added
Generated
+42 -42
View File
@@ -213,9 +213,9 @@ checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
[[package]]
name = "clap"
version = "4.6.6"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
dependencies = [
"clap_builder",
"clap_derive",
@@ -223,9 +223,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.6.6"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
dependencies = [
"anstream",
"anstyle",
@@ -235,14 +235,14 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.6.4"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 3.0.4",
"syn 3.0.5",
]
[[package]]
@@ -403,7 +403,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 3.0.4",
"syn 3.0.5",
]
[[package]]
@@ -414,7 +414,7 @@ checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785"
dependencies = [
"darling_core",
"quote",
"syn 3.0.4",
"syn 3.0.5",
]
[[package]]
@@ -710,12 +710,12 @@ dependencies = [
"indoc",
"proc-macro2",
"quote",
"syn 3.0.4",
"syn 3.0.5",
]
[[package]]
name = "irtt-client"
version = "0.5.1"
version = "0.5.2"
dependencies = [
"irtt-proto",
"irtt-server",
@@ -749,7 +749,7 @@ dependencies = [
[[package]]
name = "irtt-rs"
version = "0.7.0"
version = "0.7.1"
dependencies = [
"clap",
"crossterm",
@@ -805,9 +805,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "js-sys"
version = "0.3.104"
version = "0.3.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e"
dependencies = [
"cfg-if",
"futures-util",
@@ -887,9 +887,9 @@ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "lru"
version = "0.18.3"
version = "0.18.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d317b4b9eb398e6acce275758ec6125535505e7a146fb1a9b8bda2451b0ff4c"
checksum = "ff9840bcc50b71349309900da0ce7279aa336ae71d73250b07998932c7d97c25"
dependencies = [
"hashbrown 0.17.1",
]
@@ -943,9 +943,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "mio"
version = "1.2.2"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"
dependencies = [
"libc",
"log",
@@ -1118,9 +1118,9 @@ dependencies = [
[[package]]
name = "pest"
version = "2.9.0"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a07a60cc7a4d00c91f95c685609d1d2f79050e6804b70ebedd7650f0b839bcf"
checksum = "6d45aeb61b4bf818e12d4205f2466f8c4748f85f4fce0146d1c03d69d753f0ad"
dependencies = [
"memchr",
"ucd-trie",
@@ -1128,9 +1128,9 @@ dependencies = [
[[package]]
name = "pest_derive"
version = "2.9.0"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3a83744a5c8455b8b3e0dc5031362780a347c878bdd11584d1a8984228cc88d"
checksum = "89cc5a242e25ed4e7704d0be240f2cfbe20a8c27e7e252d94835be93d92dc39f"
dependencies = [
"pest",
"pest_generator",
@@ -1138,9 +1138,9 @@ dependencies = [
[[package]]
name = "pest_generator"
version = "2.9.0"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0cd3451aa3de60d4b9a1e736885e4dea6b31617598026f12256ad566d63304a"
checksum = "7abf21475cc3820fe4b2ca2dc2142902f67a02189f3b5b3a229f4febc01a43e5"
dependencies = [
"pest",
"pest_meta",
@@ -1151,9 +1151,9 @@ dependencies = [
[[package]]
name = "pest_meta"
version = "2.9.0"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e04d3a0849e241d7dfce834c83b1c5edc8622009e8dd51a12ba1927c32f05496"
checksum = "adba4db388f687393c18c51348d44a41d870ca9df71a2c98172ea3035dc6936e"
dependencies = [
"pest",
]
@@ -1569,7 +1569,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.4",
"syn 3.0.5",
]
[[package]]
@@ -1628,9 +1628,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.15.2"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f"
[[package]]
name = "socket2"
@@ -1705,9 +1705,9 @@ dependencies = [
[[package]]
name = "syn"
version = "3.0.4"
version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
dependencies = [
"proc-macro2",
"quote",
@@ -1840,7 +1840,7 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.4",
"syn 3.0.5",
]
[[package]]
@@ -1886,7 +1886,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.4",
"syn 3.0.5",
]
[[package]]
@@ -1995,9 +1995,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
version = "0.2.127"
version = "0.2.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"
dependencies = [
"cfg-if",
"once_cell",
@@ -2008,9 +2008,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.127"
version = "0.2.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -2018,22 +2018,22 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.127"
version = "0.2.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 2.0.119",
"syn 3.0.5",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.127"
version = "0.2.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"
dependencies = [
"unicode-ident",
]
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "irtt-rs"
version = "0.7.0"
version = "0.7.1"
description = "IRTT-compatible client, TUI, and server applications"
edition.workspace = true
license.workspace = true
@@ -47,7 +47,7 @@ server = ["dep:irtt-server", "dep:tokio"]
tui =["dep:irtt-client", "irtt-client/ancillary", "irtt-client/tokio", "dep:irtt-proto", "dep:irtt-stats", "dep:ratatui", "dep:crossterm"]
[dependencies]
clap = { version = "4.6.6", features = ["derive", "env"] }
clap = { version = "4.6.7", features = ["derive", "env"] }
crossterm = { version = "0.29.0", optional = true }
ctrlc = { version = "3.5.2", features = ["termination"] }
irtt-client = { workspace = true, optional = true }
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "irtt-client"
version = "0.5.1"
version = "0.5.2"
description = "IRTT-compatible Rust client library"
edition.workspace = true
license.workspace = true
+25
View File
@@ -242,6 +242,31 @@ impl AsyncClientTestHooks {
/// `AsyncClient` does not construct, own, or store a Tokio runtime. Its async
/// methods are polled by the caller and require a current Tokio runtime with I/O
/// and time enabled.
///
/// # Example
///
/// Use this low-level adapter when the application already owns Tokio and wants
/// to drive the session lifecycle itself. This example type-checks without
/// contacting a server.
///
/// ```no_run
/// use irtt_client::{AsyncClient, ClientConfig};
///
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
/// let mut client = AsyncClient::connect(ClientConfig::default()).await?;
/// let outcome = client.open().await?;
/// println!("opened: {outcome:?}");
/// let sent = client.send_probe().await?;
/// println!("sent: {sent:?}");
/// let received = client.recv().await?;
/// println!("received: {received:?}");
/// let timed_out = client.poll_timeouts()?;
/// println!("timeouts: {timed_out:?}");
/// let closed = client.close().await?;
/// println!("closed: {closed:?}");
/// # Ok(())
/// # }
/// ```
#[derive(Debug)]
pub struct AsyncClient {
socket: tokio::net::UdpSocket,
+25
View File
@@ -103,6 +103,31 @@ use crate::{
/// session, send probes, receive replies, poll timeouts, and close. Callers
/// that do not need to own this loop can use the unified managed API in
/// [`crate::managed`] when the `tokio` feature is enabled.
///
/// # Example
///
/// `Client` is the runtime-free adapter. The caller drives receives and timeout
/// polling between sends; see the standalone example for a complete loop.
/// This example type-checks without contacting a server:
///
/// ```no_run
/// use irtt_client::{Client, ClientConfig};
///
/// # fn run() -> Result<(), Box<dyn std::error::Error>> {
/// let mut client = Client::connect(ClientConfig::default())?;
/// let outcome = client.open()?;
/// println!("opened: {outcome:?}");
/// let sent = client.send_probe()?;
/// println!("sent: {sent:?}");
/// let received = client.recv_once()?;
/// println!("received: {received:?}");
/// let timed_out = client.poll_timeouts()?;
/// println!("timeouts: {timed_out:?}");
/// let closed = client.close()?;
/// println!("closed: {closed:?}");
/// # Ok(())
/// # }
/// ```
#[derive(Debug)]
pub struct Client {
runtime: SessionMachine,
+48
View File
@@ -46,6 +46,31 @@ pub(crate) const DEFAULT_MAX_PENDING: usize = 4096;
/// request and the local client behavior used to drive the UDP socket. Values
/// that are negotiated by the server are available after opening the session
/// through [`NegotiatedParams`](crate::NegotiatedParams).
///
/// # Example
///
/// Configure a finite run with larger probes and accept documented server
/// restrictions. Use `duration: None` for continuous mode, or
/// [`RunMode::NoTest`] for negotiation without probes.
///
/// ```
/// use std::time::Duration;
///
/// use irtt_client::{ClientConfig, NegotiationPolicy};
///
/// let config = ClientConfig {
/// server_addr: "example.net:2112".into(),
/// duration: Some(Duration::from_secs(10)),
/// interval: Duration::from_millis(250),
/// length: 1200,
/// negotiation_policy: NegotiationPolicy::Loose,
/// ..ClientConfig::default()
/// };
///
/// assert_eq!(config.duration, Some(Duration::from_secs(10)));
/// assert_eq!(config.interval, Duration::from_millis(250));
/// assert_eq!(config.length, 1200);
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClientConfig {
/// Remote server name or address.
@@ -176,6 +201,29 @@ impl Default for ClientConfig {
/// These settings affect how the client binds, resolves, and receives from the
/// socket. They do not change the IRTT protocol parameters negotiated with the
/// server.
///
/// # Example
///
/// Bind to a chosen local endpoint and set the outgoing IPv4 TTL:
///
/// ```
/// use std::net::{Ipv4Addr, SocketAddr};
///
/// use irtt_client::{ClientConfig, SocketConfig};
///
/// let config = ClientConfig {
/// socket_config: SocketConfig {
/// bind_addr: Some(SocketAddr::from((Ipv4Addr::UNSPECIFIED, 0))),
/// ttl: Some(32),
/// ipv4_only: true,
/// ..SocketConfig::default()
/// },
/// ..ClientConfig::default()
/// };
///
/// assert_eq!(config.socket_config.ttl, Some(32));
/// assert!(config.socket_config.ipv4_only);
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SocketConfig {
/// Local address to bind before connecting the UDP socket.
+13
View File
@@ -260,6 +260,19 @@ pub struct RttSample {
/// processing time or comparing wall-clock timestamps from different hosts. It
/// should not be used to represent raw elapsed wall-clock or monotonic time,
/// which is non-negative and represented by [`Duration`].
///
/// # Example
///
/// Signed timing values retain a negative result instead of clamping clock
/// skew or server processing adjustments to zero:
///
/// ```
/// use irtt_client::SignedDuration;
///
/// let adjusted_rtt = SignedDuration::from_nanos(-250_000);
/// assert!(adjusted_rtt.is_negative());
/// assert_eq!(adjusted_rtt.as_micros(), -250);
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct SignedDuration {
ns: i128,
+36
View File
@@ -44,6 +44,42 @@ const POST_DEADLINE_RECEIVE_BUDGET: usize = TARGET_WORK_BUDGET;
const MAX_BROADCAST_CHANNEL_CAPACITY: usize = usize::MAX >> 1;
/// Entry point for constructing a unified Tokio managed task.
///
/// # Example
///
/// Construct the task and its separate control handle, subscribe before
/// starting the task, then run it under the caller's Tokio runtime. A reachable
/// server is required at runtime, so this example only type-checks.
///
/// ```no_run
/// use irtt_client::{ClientConfig, ClientEvent};
/// use irtt_client::managed::{
/// ManagedClient, ManagedClientConfig, ManagedEvent, ManagedTargetConfig,
/// };
///
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
/// let config = ManagedClientConfig {
/// client: ClientConfig {
/// duration: Some(std::time::Duration::from_secs(10)),
/// ..ClientConfig::default()
/// },
/// ..ManagedClientConfig::default()
/// };
/// let targets = vec![ManagedTargetConfig::new("edge", "127.0.0.1:2112")];
/// let (task, handle) = ManagedClient::task(config, targets)?;
/// let mut events = handle.subscribe()?;
/// let driver = tokio::spawn(task);
///
/// while let Ok(event) = events.recv().await {
/// if let ManagedEvent::Client { event: ClientEvent::EchoReply { .. }, .. } = event {
/// println!("received a probe reply");
/// }
/// }
/// let outcome = driver.await?;
/// println!("run ended: {:?}", outcome.end_reason);
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Default)]
pub struct ManagedClient;
+44
View File
@@ -60,6 +60,50 @@ pub struct TimestampFields {
pub send_mono: Option<i64>,
}
/// Encodes an ECHO reply.
///
/// `params` supplies the negotiated layout and length, exactly as on the
/// request side, and every optional field the layout requires must be present
/// in `reply`: a `Some` where the layout has no field, or a `None` where it has
/// one, is an error. `hmac_key` is authoritative: `Some(key)` sets `FLAG_HMAC`
/// and signs the finished datagram.
///
/// # Errors
///
/// Returns [`ProtoError::MissingFlag`] or [`ProtoError::UnexpectedFlag`] when
/// the reply does not carry exactly the packet-type flags this encoder
/// requires, [`ProtoError::MissingField`] or [`ProtoError::UnexpectedField`]
/// when `reply`'s optional fields do not match the negotiated layout, and
/// [`ProtoError::PayloadTooLarge`] when the payload does not fit the negotiated
/// length.
///
/// # Example
///
/// ```
/// use irtt_proto::{
/// decode_echo_reply, encode_echo_reply, EchoReply, Params, ReceivedStats,
/// TimestampFields, FLAG_REPLY,
/// };
///
/// // `received_stats` negotiates a receive counter; four payload bytes.
/// let params = Params {
/// length: 24,
/// received_stats: ReceivedStats::Count,
/// ..Params::default()
/// };
/// let reply = EchoReply {
/// flags: FLAG_REPLY,
/// token: 0x1234_5678_9abc_def0,
/// sequence: 7,
/// recv_count: Some(3),
/// recv_window: None,
/// timestamps: TimestampFields::default(),
/// payload: vec![1, 2, 3, 4],
/// };
///
/// let packet = encode_echo_reply(&reply, &params, None).unwrap();
/// assert_eq!(decode_echo_reply(&packet, &params, None).unwrap(), reply);
/// ```
pub fn encode_echo_reply(
reply: &EchoReply,
params: &Params,
+17
View File
@@ -68,6 +68,23 @@ pub fn verify_hmac(key: &[u8], packet: &[u8], hmac_offset: usize) -> Result<()>
/// [`ProtoError::InvalidHmacOffset`] when the field is truncated, and
/// [`ProtoError::BadHmac`] when the MAC does not match. The packet is never
/// modified.
///
/// # Example
///
/// ```
/// use irtt_proto::{encode_request, verify_packet_hmac, Params, RequestToEncode};
///
/// // Synthetic example key — use a real shared secret in production.
/// let key = b"example key";
/// let params = Params::with_protocol_defaults();
/// let packet = encode_request(
/// RequestToEncode::Open { params: &params, no_test: false },
/// Some(key),
/// ).unwrap();
///
/// verify_packet_hmac(key, &packet).unwrap();
/// assert!(verify_packet_hmac(b"wrong key", &packet).is_err());
/// ```
pub fn verify_packet_hmac(key: &[u8], packet: &[u8]) -> Result<()> {
let envelope = envelope::decode_structural(packet)?;
if !envelope.hmac_present {
+14
View File
@@ -135,6 +135,20 @@ pub fn echo_header_len(hmac: bool, params: &Params) -> usize {
/// A ceiling on what a negotiated length may legitimately *be* — an MTU, a
/// resource bound, a maximum packet size — is deliberately not here. That is
/// server and runtime policy, and it belongs where the negotiation happens.
///
/// # Example
///
/// ```
/// use irtt_proto::{echo_packet_len, Params};
///
/// // The default layout's mandatory field block is 16 bytes (header, token,
/// // sequence), so a smaller requested length is floored up to it.
/// assert_eq!(echo_packet_len(false, &Params::default()).unwrap(), 16);
/// assert_eq!(
/// echo_packet_len(false, &Params { length: 40, ..Params::default() }).unwrap(),
/// 40,
/// );
/// ```
pub fn echo_packet_len(hmac: bool, params: &Params) -> Result<usize> {
let header_len = echo_header_len(hmac, params);
let requested = if params.length <= 0 {
+33
View File
@@ -42,6 +42,39 @@
//! which reply kind to expect, and the two need genuinely different semantic
//! context.
//!
//! # Example
//!
//! The normal path is directional: a sender encodes with [`encode_request`], a
//! receiver classifies with [`decode_request`], and authentication is a
//! separate step once the applicable key is known.
//!
//! ```
//! use irtt_proto::{
//! decode_request, encode_request, verify_packet_hmac, DecodedRequestKind, Params,
//! RequestToEncode,
//! };
//!
//! // Sender side: encode an authenticated open request.
//! let params = Params::with_protocol_defaults();
//! let key = b"example key";
//! let packet = encode_request(
//! RequestToEncode::Open { params: &params, no_test: false },
//! Some(key),
//! ).unwrap();
//!
//! // Receiver side: classify structurally, before any key is known.
//! let request = decode_request(&packet).unwrap();
//! assert!(request.hmac_present);
//! let DecodedRequestKind::Open { no_test, params: encoded } = request.kind else {
//! panic!("expected an open request");
//! };
//! assert!(!no_test);
//! assert_eq!(Params::decode(encoded).unwrap(), params);
//!
//! // Authentication is a separate step, once the applicable key is known.
//! verify_packet_hmac(key, &packet).unwrap();
//! ```
//!
#![forbid(unsafe_code)]
pub mod echo;
+40
View File
@@ -13,6 +13,34 @@ pub struct OpenReply {
pub params: Params,
}
/// Encodes an open reply.
///
/// `hmac_key` is authoritative: `Some(key)` sets `FLAG_HMAC` and signs the
/// finished datagram, while `None` produces an unauthenticated reply and clears
/// any caller-supplied `FLAG_HMAC`. A zero token is rejected unless the reply
/// also carries `FLAG_CLOSE` (a rejected open).
///
/// # Errors
///
/// Returns [`ProtoError::MissingFlag`] when the reply lacks `FLAG_OPEN` or
/// `FLAG_REPLY`, and [`ProtoError::ZeroToken`] for a zero token without
/// `FLAG_CLOSE`.
///
/// # Example
///
/// ```
/// use irtt_proto::{
/// decode_open_reply, encode_open_reply, OpenReply, Params, FLAG_OPEN, FLAG_REPLY,
/// };
///
/// let reply = OpenReply {
/// flags: FLAG_OPEN | FLAG_REPLY,
/// token: 0x1234_5678_9abc_def0,
/// params: Params::with_protocol_defaults(),
/// };
/// let packet = encode_open_reply(&reply, None).unwrap();
/// assert_eq!(decode_open_reply(&packet, None).unwrap(), reply);
/// ```
pub fn encode_open_reply(reply: &OpenReply, hmac_key: Option<&[u8]>) -> Result<Vec<u8>> {
let params = reply.params.encode();
let mut out = envelope::begin_checked(
@@ -30,6 +58,18 @@ pub fn encode_open_reply(reply: &OpenReply, hmac_key: Option<&[u8]>) -> Result<V
envelope::finish(out, hmac_key)
}
/// Decodes an open reply.
///
/// Requires `FLAG_OPEN` and `FLAG_REPLY`, and — when `hmac_key` is supplied —
/// verifies the authentication field. The decoded [`Params`] are validated:
/// malformed or incompatible parameter values are rejected here.
///
/// # Errors
///
/// Returns [`ProtoError::MissingFlag`], [`ProtoError::HmacPresenceMismatch`],
/// [`ProtoError::BadHmac`], [`ProtoError::PacketTooShort`],
/// [`ProtoError::ZeroToken`], or a parameter decode error. See
/// [`encode_open_reply`] for a round-trip example.
pub fn decode_open_reply(packet: &[u8], hmac_key: Option<&[u8]>) -> Result<OpenReply> {
let envelope = envelope::decode(
packet,
+37
View File
@@ -16,6 +16,29 @@ pub const MAX_SERVER_FILL_BYTES: usize = 32;
/// [`clock`](Params::clock) is [`Clock::Unspecified`], which is what an open
/// request with an empty parameter payload means. It is not a set of sensible
/// client settings; a client builds its request from its own configuration.
///
/// # Example
///
/// Construct a `Params`, encode it, and decode it back:
///
/// ```
/// use irtt_proto::{Clock, Params, ReceivedStats, ServerFill, StampAt};
///
/// let params = Params {
/// protocol_version: 1,
/// duration_ns: 3_000_000_000,
/// interval_ns: 1_000_000_000,
/// length: 1472,
/// received_stats: ReceivedStats::Both,
/// stamp_at: StampAt::Both,
/// clock: Clock::Both,
/// server_fill: Some(ServerFill { value: "rand".to_owned() }),
/// ..Params::default()
/// };
///
/// let encoded = params.encode();
/// assert_eq!(Params::decode(&encoded).unwrap(), params);
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Params {
pub protocol_version: i64,
@@ -88,6 +111,20 @@ impl Params {
/// Presence means the tag appeared at least once. Repeated known tags keep
/// last-value-wins, and unknown tags remain ignored and untracked.
///
/// # Example
///
/// ```
/// use irtt_proto::Params;
///
/// // Wire bytes: tag 2 (duration) carrying an explicit zero. Interval
/// // (tag 3) is absent, so both fields decode to zero but only one is
/// // reported present.
/// let decoded = Params::decode_with_presence(&[0x02, 0x00]).unwrap();
/// assert_eq!(decoded.params.duration_ns, 0);
/// assert!(decoded.presence.duration_ns);
/// assert!(!decoded.presence.interval_ns);
/// ```
///
/// [`decode`]: Params::decode
pub fn decode_with_presence(input: &[u8]) -> Result<DecodedParams> {
let mut params = Self::default();
+35
View File
@@ -88,6 +88,26 @@ pub enum DecodedRequestKind<'a> {
/// the negotiated length is positive and wider than `usize`. A *negative*
/// negotiated length is not an error: see [`echo_packet_len`], which floors it
/// at the mandatory field block.
///
/// # Example
///
/// ```
/// use irtt_proto::{encode_request, Params, RequestToEncode};
///
/// // An unauthenticated ECHO probe. `params` supplies the negotiated length,
/// // which reserves four payload bytes beyond the 16-byte echo header.
/// let params = Params { length: 20, ..Params::default() };
/// let packet = encode_request(
/// RequestToEncode::Echo {
/// token: 0x1234_5678_9abc_def0,
/// sequence: 7,
/// params: &params,
/// payload: &[1, 2, 3, 4],
/// },
/// None,
/// ).unwrap();
/// assert_eq!(packet.len(), 20);
/// ```
pub fn encode_request(request: RequestToEncode<'_>, hmac_key: Option<&[u8]>) -> Result<Vec<u8>> {
match request {
RequestToEncode::Open { params, no_test } => encode_open(params, no_test, hmac_key),
@@ -138,6 +158,21 @@ pub fn encode_request(request: RequestToEncode<'_>, hmac_key: Option<&[u8]>) ->
/// Returns [`ProtoError::PacketTooShort`], [`ProtoError::BadMagic`],
/// [`ProtoError::ReservedFlags`], or [`ProtoError::UnexpectedFlag`] for
/// `FLAG_REPLY`.
///
/// # Example
///
/// ```
/// use irtt_proto::{decode_request, encode_request, DecodedRequestKind, RequestToEncode};
///
/// let packet = encode_request(
/// RequestToEncode::Close { token: 0x1234_5678_9abc_def0 },
/// None,
/// ).unwrap();
///
/// let request = decode_request(&packet).unwrap();
/// assert!(!request.hmac_present);
/// assert_eq!(request.kind, DecodedRequestKind::Close { token: 0x1234_5678_9abc_def0 });
/// ```
pub fn decode_request(packet: &[u8]) -> Result<DecodedRequest<'_>> {
let envelope = envelope::decode_structural(packet)?;
if has(envelope.flags, FLAG_REPLY) {
+16
View File
@@ -1,3 +1,19 @@
//! Variable-length integer encoding for IRTT wire values.
//!
//! Tags use an unsigned base-128 varint ([`encode_uvarint`]), and signed
//! integers (such as parameter values) use the same encoding after a zigzag
//! transform ([`encode_varint`]), which keeps small negative numbers compact.
//!
//! # Example
//!
//! ```
//! use irtt_proto::varint::{decode_varint, encode_varint};
//!
//! let mut out = Vec::new();
//! encode_varint(1_000_000_000, &mut out);
//! assert_eq!(decode_varint(&out).unwrap(), (1_000_000_000, out.len()));
//! ```
use crate::{ProtoError, Result};
pub fn encode_uvarint(mut value: u64, out: &mut Vec<u8>) {
+22
View File
@@ -98,6 +98,28 @@ pub enum TimestampAllowance {
/// [`max_packet_length`](Self::max_packet_length) like every other reply, and
/// `none` is zero-filled here rather than left as residue. An operator
/// restriction can be added if a real use case turns up.
///
/// # Example
///
/// Configure a modest per-listener session budget and restrict timestamp
/// placement while leaving DSCP marking disabled:
///
/// ```
/// use std::time::Duration;
///
/// use irtt_server::{ServerConfig, TimestampAllowance};
///
/// let config = ServerConfig::default()
/// .with_max_sessions(128)
/// .with_idle_timeout(Duration::from_secs(30))
/// .with_timestamp_allowance(TimestampAllowance::Single)
/// .with_dscp_allowed(false);
///
/// assert_eq!(config.max_sessions(), 128);
/// assert_eq!(config.idle_timeout(), Duration::from_secs(30));
/// assert_eq!(config.timestamp_allowance(), TimestampAllowance::Single);
/// assert!(!config.dscp_allowed());
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ServerConfig {
hmac_key: Option<Vec<u8>>,
+26
View File
@@ -47,6 +47,32 @@ use crate::{
/// a transport abstraction, and there is no blocking or alternate-runtime
/// counterpart.
///
/// # Example
///
/// The core can be driven directly with a normally encoded open request. It
/// returns a reply to send and records the new session without performing I/O:
///
/// ```
/// use std::net::{Ipv4Addr, SocketAddr};
///
/// use irtt_proto::{decode_open_reply, encode_request, Params, RequestToEncode};
/// use irtt_server::{ServerConfig, ServerCore};
///
/// let peer = SocketAddr::from((Ipv4Addr::LOCALHOST, 40_000));
/// let mut core = ServerCore::new(ServerConfig::default());
/// let params = Params::with_protocol_defaults();
/// let request = encode_request(
/// RequestToEncode::Open { params: &params, no_test: false },
/// None,
/// )
/// .unwrap();
///
/// let reply = core.handle_datagram(peer, &request).unwrap().unwrap();
/// let open = decode_open_reply(reply.bytes(), None).unwrap();
/// assert_ne!(open.token, 0);
/// assert_eq!(core.session_count(), 1);
/// ```
///
/// # Scope
///
/// The core implements open handling, session creation and negotiation,
+22
View File
@@ -53,6 +53,28 @@ const MAINTENANCE_INTERVAL: Duration = Duration::from_secs(1);
/// [`ServerRuntimeError::WildcardSourceSelectionUnsupported`] — rather than
/// served by a listener whose replies a client on a second address would
/// silently discard. Explicit-address listeners are unaffected everywhere.
///
/// # Example
///
/// Bind one listener, inspect the resolved endpoint, and let the caller decide
/// when serving ends. This example type-checks without opening a socket:
///
/// ```no_run
/// use std::{net::{Ipv4Addr, SocketAddr}, time::Duration};
///
/// use irtt_server::{Server, ServerConfig};
///
/// # async fn serve() -> Result<(), Box<dyn std::error::Error>> {
/// let mut server = Server::bind(
/// SocketAddr::from((Ipv4Addr::LOCALHOST, 0)),
/// ServerConfig::default(),
/// )
/// .await?;
/// println!("listening on {}", server.local_addr()?);
/// server.run(tokio::time::sleep(Duration::from_secs(60))).await?;
/// # Ok(())
/// # }
/// ```
#[derive(Debug)]
pub struct Server {
socket: UdpSocket,
+23
View File
@@ -57,6 +57,29 @@ use crate::{Server, ServerConfig, ServerRuntimeError};
/// whole set: a service configured for IPv4 and IPv6 must not quietly continue
/// as IPv4 only. Every listener task is joined before `run` returns, so no task
/// outlives it.
///
/// # Example
///
/// Bind independent listeners, inspect their resolved addresses, then serve
/// them under one caller-owned shutdown future. This example type-checks
/// without binding sockets:
///
/// ```no_run
/// use std::{net::{Ipv4Addr, SocketAddr}, time::Duration};
///
/// use irtt_server::{ServerConfig, ServerSet};
///
/// # async fn serve() -> Result<(), Box<dyn std::error::Error>> {
/// let addrs = [
/// SocketAddr::from((Ipv4Addr::LOCALHOST, 0)),
/// SocketAddr::from((Ipv4Addr::LOCALHOST, 0)),
/// ];
/// let set = ServerSet::bind(addrs, ServerConfig::default()).await?;
/// println!("listening on {:?}", set.local_addrs());
/// set.run(tokio::time::sleep(Duration::from_secs(60))).await?;
/// # Ok(())
/// # }
/// ```
#[derive(Debug)]
pub struct ServerSet {
servers: Vec<Server>,
+246
View File
@@ -66,6 +66,44 @@ impl StatsConfig {
/// them reports a median in the cumulative snapshot once it has samples,
/// and keeps unbounded adjacent-sequence IPDV tracking so late adjacent
/// replies can still complete IPDV pairs.
///
/// # Example
///
/// ```
/// use irtt_stats::{StatsCollector, StatsConfig};
///
/// # use std::net::{IpAddr, Ipv4Addr, SocketAddr};
/// # use std::time::{Duration, Instant, SystemTime};
/// # use irtt_client::{ClientEvent, ClientTimestamp};
/// #
/// # fn sent(seq: u32, at_ms: u64, send_call_us: u64) -> ClientEvent {
/// # let sent_at = ClientTimestamp {
/// # mono: Instant::now() + Duration::from_millis(at_ms),
/// # wall: SystemTime::UNIX_EPOCH + Duration::from_millis(at_ms),
/// # };
/// # ClientEvent::EchoSent {
/// # seq,
/// # remote: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 2112),
/// # scheduled_at: sent_at.mono,
/// # sent_at,
/// # bytes: 64,
/// # send_call: Duration::from_micros(send_call_us),
/// # timer_error: Duration::from_micros(2),
/// # }
/// # }
/// #
/// let mut collector = StatsCollector::new(StatsConfig::finite());
/// for (seq, send_call_us) in [10_u64, 30, 20].into_iter().enumerate() {
/// collector.process(&sent(seq as u32, seq as u64 * 10, send_call_us));
/// }
///
/// let snapshot = collector.snapshot();
/// // Finite mode retains exact samples, so the send-call metric reports an
/// // exact median once it has samples: the middle of the sorted
/// // 10, 20, 30 µs values, not their mean.
/// assert_eq!(snapshot.send_call.count, 3);
/// assert_eq!(snapshot.send_call.median_ns, Some(20_000.0));
/// ```
pub fn finite() -> Self {
Self {
samples: SampleMode::Exact,
@@ -80,6 +118,44 @@ impl StatsConfig {
/// Continuous mode uses running statistics, retains no exact samples so no
/// timing metric reports a median, and bounds adjacent-sequence IPDV
/// tracking for long-running sessions.
///
/// # Example
///
/// ```
/// use irtt_stats::{StatsCollector, StatsConfig};
///
/// # use std::net::{IpAddr, Ipv4Addr, SocketAddr};
/// # use std::time::{Duration, Instant, SystemTime};
/// # use irtt_client::{ClientEvent, ClientTimestamp};
/// #
/// # fn sent(seq: u32, at_ms: u64, send_call_us: u64) -> ClientEvent {
/// # let sent_at = ClientTimestamp {
/// # mono: Instant::now() + Duration::from_millis(at_ms),
/// # wall: SystemTime::UNIX_EPOCH + Duration::from_millis(at_ms),
/// # };
/// # ClientEvent::EchoSent {
/// # seq,
/// # remote: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 2112),
/// # scheduled_at: sent_at.mono,
/// # sent_at,
/// # bytes: 64,
/// # send_call: Duration::from_micros(send_call_us),
/// # timer_error: Duration::from_micros(2),
/// # }
/// # }
/// #
/// let mut collector = StatsCollector::new(StatsConfig::continuous());
/// for seq in 0..3 {
/// collector.process(&sent(seq, u64::from(seq) * 10, 10));
/// }
///
/// let snapshot = collector.snapshot();
/// // Continuous mode still tracks running statistics — count, mean, min,
/// // and max — but retains no exact samples, so it reports no median.
/// assert_eq!(snapshot.send_call.count, 3);
/// assert!(snapshot.send_call.mean_ns > 0.0);
/// assert_eq!(snapshot.send_call.median_ns, None);
/// ```
pub fn continuous() -> Self {
Self {
samples: SampleMode::RunningOnly,
@@ -135,6 +211,34 @@ impl StatsConfig {
/// The result is deterministic and computed with saturating arithmetic, so
/// an enormous `probe_count` saturates at [`u64::MAX`] rather than
/// wrapping or panicking.
///
/// # Example
///
/// ```
/// use irtt_stats::StatsConfig;
///
/// // Finite retention grows with the probe count...
/// let finite = StatsConfig::finite();
/// assert!(
/// finite.estimated_retained_bytes(1_000_000)
/// > finite.estimated_retained_bytes(1_000)
/// );
///
/// // ...while continuous retention is bounded, so a far longer run is no
/// // more expensive.
/// let continuous = StatsConfig::continuous();
/// assert_eq!(
/// continuous.estimated_retained_bytes(1_000_000),
/// continuous.estimated_retained_bytes(10_000_000),
/// );
/// assert!(
/// continuous.estimated_retained_bytes(1_000_000)
/// < finite.estimated_retained_bytes(1_000_000)
/// );
///
/// // Enormous probe counts saturate rather than wrapping or panicking.
/// assert_eq!(finite.estimated_retained_bytes(u64::MAX), u64::MAX);
/// ```
pub fn estimated_retained_bytes(&self, probe_count: u64) -> u64 {
retention::estimated_retained_bytes(self, probe_count)
}
@@ -192,6 +296,60 @@ pub enum SampleMode {
///
/// A collector maintains cumulative statistics and, when configured, rolling
/// windows. Rolling snapshots are recomputed from retained normalized events.
///
/// # Example
///
/// Feed a short stream of `irtt-client` events to a finite-mode collector and
/// read back a cumulative snapshot:
///
/// ```
/// use irtt_stats::{StatsCollector, StatsConfig};
///
/// # use std::net::{IpAddr, Ipv4Addr, SocketAddr};
/// # use std::time::{Duration, Instant, SystemTime};
/// # use irtt_client::{ClientEvent, ClientTimestamp, RttSample, SignedDuration};
/// #
/// # /// A synthetic reply; a real session delivers these as `ClientEvent`s.
/// # fn reply(seq: u32, rtt_ms: u64) -> ClientEvent {
/// # let remote = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 2112);
/// # let sent_at = ClientTimestamp {
/// # mono: Instant::now() + Duration::from_millis(u64::from(seq) * 10),
/// # wall: SystemTime::UNIX_EPOCH + Duration::from_millis(u64::from(seq) * 10),
/// # };
/// # let raw = Duration::from_millis(rtt_ms);
/// # ClientEvent::EchoReply {
/// # seq,
/// # remote,
/// # sent_at,
/// # received_at: ClientTimestamp {
/// # mono: sent_at.mono + raw,
/// # wall: sent_at.wall + raw,
/// # },
/// # rtt: RttSample {
/// # raw,
/// # adjusted: None,
/// # effective: SignedDuration::from_duration(raw),
/// # },
/// # server_timing: None,
/// # one_way: None,
/// # received_stats: None,
/// # bytes: 64,
/// # packet_meta: irtt_client::PacketMeta::default(),
/// # }
/// # }
/// #
/// let mut collector = StatsCollector::new(StatsConfig::finite());
/// for (seq, rtt_ms) in [10_u64, 30, 20].into_iter().enumerate() {
/// collector.process(&reply(seq as u32, rtt_ms));
/// }
///
/// let snapshot = collector.snapshot();
/// assert_eq!(snapshot.events.echo_replies, 3);
/// assert_eq!(snapshot.rtt.primary.count, 3);
/// // Finite mode retains exact samples, so the round-trip metric reports an
/// // exact median: the middle of the sorted 10, 20, 30 ms samples.
/// assert_eq!(snapshot.rtt.primary.median_ns, Some(20_000_000.0));
/// ```
pub struct StatsCollector {
cumulative: CoreStats,
rolling: RollingEvents,
@@ -236,6 +394,53 @@ impl StatsCollector {
/// running statistics only, whatever [`StatsConfig::samples`] is set to,
/// so its timing metrics report no median. A rolling window is a bounded
/// recent view rather than the run's retained history.
///
/// # Example
///
/// ```
/// use irtt_stats::{StatsCollector, StatsConfig};
///
/// # use std::net::{IpAddr, Ipv4Addr, SocketAddr};
/// # use std::time::{Duration, Instant, SystemTime};
/// # use irtt_client::{ClientEvent, ClientTimestamp};
/// #
/// # fn sent(seq: u32, at_ms: u64, send_call_us: u64) -> ClientEvent {
/// # let sent_at = ClientTimestamp {
/// # mono: Instant::now() + Duration::from_millis(at_ms),
/// # wall: SystemTime::UNIX_EPOCH + Duration::from_millis(at_ms),
/// # };
/// # ClientEvent::EchoSent {
/// # seq,
/// # remote: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 2112),
/// # scheduled_at: sent_at.mono,
/// # sent_at,
/// # bytes: 64,
/// # send_call: Duration::from_micros(send_call_us),
/// # timer_error: Duration::from_micros(2),
/// # }
/// # }
/// #
/// let mut collector = StatsCollector::new(StatsConfig {
/// rolling_count: Some(2),
/// ..StatsConfig::finite()
/// });
/// collector.process(&sent(0, 0, 10));
/// collector.process(&sent(1, 10, 10));
/// collector.process(&sent(2, 20, 10));
///
/// // The window keeps only the last two normalized events...
/// let rolling = collector.rolling_count().unwrap();
/// assert_eq!(rolling.packets.packets_sent, 2);
/// // ...and is recomputed with running statistics, so it reports no median
/// // even though the collector retains exact samples for its cumulative
/// // snapshot.
/// assert_eq!(rolling.send_call.median_ns, None);
///
/// // The cumulative snapshot still sees every event.
/// let cumulative = collector.snapshot();
/// assert_eq!(cumulative.packets.packets_sent, 3);
/// assert_eq!(cumulative.send_call.median_ns, Some(10_000.0));
/// ```
pub fn rolling_count(&self) -> Option<Snapshot> {
self.rolling.count_snapshot()
}
@@ -244,6 +449,47 @@ impl StatsCollector {
///
/// Like [`StatsCollector::rolling_count`], this is recomputed with running
/// statistics only and reports no medians.
///
/// # Example
///
/// ```
/// use std::time::Duration;
///
/// use irtt_stats::{StatsCollector, StatsConfig};
///
/// # use std::net::{IpAddr, Ipv4Addr, SocketAddr};
/// # use std::time::Instant;
/// # use irtt_client::{ClientEvent, ClientTimestamp};
/// #
/// # fn sent(seq: u32, at_ms: u64, send_call_us: u64) -> ClientEvent {
/// # let sent_at = ClientTimestamp {
/// # mono: Instant::now() + Duration::from_millis(at_ms),
/// # wall: std::time::SystemTime::UNIX_EPOCH + Duration::from_millis(at_ms),
/// # };
/// # ClientEvent::EchoSent {
/// # seq,
/// # remote: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 2112),
/// # scheduled_at: sent_at.mono,
/// # sent_at,
/// # bytes: 64,
/// # send_call: Duration::from_micros(send_call_us),
/// # timer_error: Duration::from_micros(2),
/// # }
/// # }
/// #
/// let mut collector = StatsCollector::new(StatsConfig {
/// rolling_time: Some(Duration::from_millis(15)),
/// ..StatsConfig::finite()
/// });
/// collector.process(&sent(0, 0, 10));
/// collector.process(&sent(1, 10, 10));
/// collector.process(&sent(2, 30, 10));
///
/// // A 15 ms window anchored at the latest event (30 ms) retains only the
/// // events from the last 15 ms — here just the final send.
/// let rolling = collector.rolling_time().unwrap();
/// assert_eq!(rolling.packets.packets_sent, 1);
/// ```
pub fn rolling_time(&self) -> Option<Snapshot> {
self.rolling.time_snapshot()
}
-20
View File
@@ -13,26 +13,6 @@ use irtt_stats::{
EventStatsUpdate, IpdvPairUpdate, LateReplyMode, SampleMode, StatsCollector, StatsConfig,
};
#[test]
fn exact_mode_reports_a_send_call_median() {
let mut collector = StatsCollector::new(StatsConfig::finite());
for (seq, send_call_us) in [10_u64, 30, 20].into_iter().enumerate() {
collector.process(&sent_with_timings(
seq as u32,
ts(seq as u64 * 10),
send_call_us,
2,
));
}
let snapshot = collector.snapshot();
assert_eq!(snapshot.send_call.count, 3);
assert_eq!(snapshot.send_call.min_ns, Some(10_000));
assert_eq!(snapshot.send_call.max_ns, Some(30_000));
// Sorted 10, 20, 30 us: the median is the middle sample, not the mean.
assert_eq!(snapshot.send_call.median_ns, Some(20_000.0));
}
#[test]
fn exact_mode_reports_a_timer_error_median() {
let mut collector = StatsCollector::new(StatsConfig::finite());
+6 -6
View File
@@ -31,9 +31,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.4.4"
version = "1.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273"
checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -97,9 +97,9 @@ dependencies = [
[[package]]
name = "find-msvc-tools"
version = "0.1.11"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"
[[package]]
name = "getrandom"
@@ -285,9 +285,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "3.0.4"
version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
dependencies = [
"proc-macro2",
"quote",