Repository navigation
Adopt the exact counter sum from torrust-metrics 0.2.0 #2560
Description
Goal
Upgrade torrust-metrics to 0.2.0 in every package that uses it, and read integer counter totals through the new sum_counter API, so the tracker no longer converts a u64 counter through f64 and back.
Background
Issue #2244 (PR #2552) reviewed the casts at the tracker's metric boundaries. The public aggregate trait of torrust-metrics 0.1.0 returns Option<f64> for both counters and gauges, so a u64 counter sum was converted to f64 inside the crate and back to u64 in the tracker. #2244 funneled the tracker's side of that round trip into one private whole_total helper per package (HTTP core, UDP core, UDP server). Each helper carries one #[expect] whose reason documents the invariant: a counter sum is a finite, non-negative whole number, exact up to 2^53. The reason is deliberately permanent, so #2244 did not depend on an upstream release.
Upstream issue torrust/torrust-metrics#1 asked for an exact u64 counter sum. It was implemented in torrust/torrust-metrics#2 and released as torrust-metrics 0.2.0 (release: https://github.com/torrust/torrust-metrics/releases/tag/v0.2.0). The new trait is torrust_metrics::metric_collection::aggregate::sum::SumCounter, with fn sum_counter(&self, &MetricName, &LabelSet) -> Option<u64>. It returns None when the metric does not exist or is a gauge, and its addition is unchecked (it panics on overflow in debug builds and wraps in release builds, like Metric<Counter>::sum). Sum and Avg are unchanged.
Cargo treats 0.1.0 and 0.2.0 as incompatible, and MetricCollection appears in public structs that cross package boundaries, so all eight manifests that depend on torrust-metrics must move together. A partial bump would put two copies of the crate, and two unrelated MetricCollection types, in one build.
One conversion cannot go away. The UDP server's udp_banned_ips_total reads a gauge, because the banned-IP count falls when BanService::reset_bans clears the bans. sum_counter returns None for a gauge name, so that accessor keeps an f64 to u64 cast. The maintainer asked for the cleanest and most maintainable outcome regardless of effort. The decision is to keep that one cast in one small helper with a permanent reason, for these reasons:
- It is a real gauge, so converting it to a counter would change how the metric behaves for every consumer of the metrics.
- Asking a float-gauge library for an integer gauge accessor would widen its API, and fix a rounding policy, for one consumer.
- After this issue the cast is the only one left in the counter and gauge read path, with one documented reason.
Scope
In Scope
- Bump
torrust-metricsfrom0.1.0to0.2.0in the eight manifests:axum-rest-api-server,http-core,rest-api-protocol,rest-api-runtime-adapter,swarm-coordination-registry,tracker-core,udp-core, andudp-server. UpdateCargo.lockand check that only expected entries change. - Replace the
whole_totalhelpers of HTTP core and UDP core withsum_counter, and remove their#[expect]attributes. - In the UDP server, read counters with
sum_counterand keep one gauge helper, renamed to say it reads a gauge, with a retained#[expect]and a permanent reason forudp_banned_ips_totalonly. - Add a focused test per package that a counter above 2^53 is returned exactly. The old path rounded it.
- Keep the behavior of every accessor for realistic values, including
0for a metric that does not exist yet.
Out of Scope
averaged_processing_time_nsand the moving-average code in the UDP server. They read gauges and averages, which are fractional.- Changing the banned-IP metric from a gauge to a counter, or adding an integer gauge accessor upstream.
- Rewriting the archived Inventory Existing Clippy Allows #2158 inventory or the archived Review Metric Aggregate Conversions #2244 spec. They record history. Their text mentions
whole_totaland Add an exact u64 sum aggregate for counters torrust-metrics#1, and that stays accurate as a record. - Any other dependency update, and any change to
torrust-metricsitself.
Architectural Decisions
- Related ADRs: None.
- ADRs to create: None known. This is a dependency bump and a local refactor behind private helpers.
Design and Ownership Review
Not applicable: this work changes dependency versions and local conversion boundaries, and does not introduce child processes, asynchronous I/O, network readiness, resource cleanup, or reusable test fixtures.
Bug-Fix Process
Not applicable: this is a dependency adoption and refactor. No defect is being fixed.
Regression Test Strategy
Not applicable: this is not a bug. The new tests protect the one behavior that differs after the change, the exact value above 2^53, and are described in the Implementation Plan.
Implementation Plan
Status values: TODO, IN_PROGRESS, BLOCKED, DONE.
| ID | Status | Task | Notes / Expected Output |
|---|---|---|---|
| T1 | TODO | Bump torrust-metrics to 0.2.0 in the eight manifests and Cargo.lock |
cargo build --workspace passes. git diff Cargo.lock is reviewed and records any transitive change, which should be only torrust-metrics itself. Follow the update-dependencies skill. |
| T2 | TODO | Read counters through sum_counter in HTTP core and UDP core, with a test that a counter above 2^53 is exact |
The whole_total helpers and their #[expect] attributes are gone. Each package has a test that fails on the old code and passes on the new. |
| T3 | TODO | Do the same in the UDP server and keep a gauge-only helper for udp_banned_ips_total |
One retained #[expect] with a permanent reason that names the gauge and the reset_bans fall. udp_processed_requests_total and every counter accessor use sum_counter. |
| T4 | TODO | Run the workspace checks and the manual scenarios | Clippy with -D warnings, the affected package tests, linter all, and pre-push pass. Evidence is recorded. |
The tests for T2 and T3 prove they fail on the old code with the mutate-then-restore method: temporarily route the accessor back through Sum::sum and as u64, observe the failure, restore.
Commit Points
| Task | Coherent change set | Commit policy |
|---|---|---|
| T1 | The eight manifests and Cargo.lock |
Commit after cargo build --workspace and a review of the lock diff. |
| T2 | HTTP core and UDP core helpers and tests, one commit per package | Commit after focused validation and required review. |
| T3 | UDP server helpers and tests | Commit after focused validation and required review. |
| T4 | Verification evidence | Commit after the checks pass. |
Progress Tracking
Workflow Checkpoints
- Folder-style spec drafted in
docs/issues/drafts/adopt-torrust-metrics-sum-counter/ISSUE.md - Spec reviewed and approved by user/maintainer
- GitHub issue created and issue number added to this spec
- Implementation completed
- Automatic verification completed (
linter all, relevant tests, and any pre-push checks) - Manual verification scenarios executed and recorded in issue-local
manual-verification-evidence.md - Acceptance criteria reviewed after implementation and updated with evidence
- Evidence-based implementation completion review recorded
- Reviewer validated acceptance criteria and updated checkboxes
- Committer verified spec progress is up to date before commit
- Issue closed and spec moved from
docs/issues/open/todocs/issues/closed/
Progress Log
- 2026-10-19 10:26 UTC - GitHub Copilot - Drafted after
torrust-metrics0.2.0 was released with the exact counter sum; the maintainer chose one task issue with one implementation PR that carries this spec, and asked for the cleanest long-term outcome for the banned-IP gauge - Awaiting maintainer review
Acceptance Criteria
- AC1: All eight manifests depend on
torrust-metrics0.2.0, andCargo.lockresolves a singletorrust-metricsversion. - AC2: HTTP core and UDP core read counter totals with
sum_counterand have no float-to-integer cast and no#[expect]in their metrics modules. - AC3: The UDP server reads counter totals with
sum_counter. Its only remaining float-to-integer cast for these totals is the gauge helper, with one#[expect]whose reason names the gauge and thereset_bansfall. - AC4: A counter above 2^53 is returned exactly by an accessor in each of the three packages, and each test is proven red against the old code.
- AC5: Every accessor returns the same value as before for realistic values, including
0for a metric that does not exist yet. The existing tests of the three packages pass unchanged. -
linter allexits with code0 - Relevant tests pass
- Manual verification scenarios are executed and documented in issue-local
manual-verification-evidence.md - Acceptance criteria are re-reviewed after implementation and reflect actual behavior
- Documentation is updated when behavior or workflow changes
Verification Plan
Automatic Checks
cargo build --workspacecargo clippy -p torrust-tracker-http-core -p torrust-tracker-udp-core -p torrust-tracker-udp-server --all-targets --all-features -- -D warnings.#[expect]makes Clippy fail when a lint stops firing, so a stale exception is detected.cargo testfor the packages that depend ontorrust-metricsand for the packages that consume their statistics.cargo machete --with-metadata,linter all, and the pre-push script.
Manual Verification Scenarios
Status values: TODO, IN_PROGRESS, DONE, FAILED, BLOCKED.
| ID | Scenario | Human-oriented command/steps | Expected Result | Status | Evidence |
|---|---|---|---|---|---|
| M1 | UDP counters over the REST API | Run a local release tracker on loopback ports. Send 1,000 sequential BEP 15 connect requests, then read GET /api/v1/stats. |
udp4_requests, udp4_connections_handled, and udp4_responses each read exactly 1,000. |
TODO | manual-verification-evidence.md section V1 |
| M2 | HTTP counters and the banned-IP gauge | On the same tracker, send a known number of HTTP announce requests with curl and read the stats. Read udp_banned_ips_total too. |
tcp4_announces_handled equals the number sent, and udp_banned_ips_total is a whole number matching the ban service. |
TODO | manual-verification-evidence.md section V2 |
Notes:
- Manual verification is mandatory even when automated tests pass.
- Every recorded validation command result identifies the toolchain that produced it when one can affect behavior, for example the nightly Rust toolchain for
cargo +nightly fmt. - Create
manual-verification-evidence.mdfromdocs/templates/MANUAL-VERIFICATION-EVIDENCE.md. - If a scenario fails, record the failure and diagnosis in the progress log before proceeding.
Disposable Verification Scripts
None planned. M1 and M2 use curl and the maintained tracker_client binary.
Acceptance Verification
| AC ID | Status (TODO/DONE) |
Evidence |
|---|---|---|
| AC1 | TODO | Pending implementation. |
| AC2 | TODO | Pending implementation. |
| AC3 | TODO | Pending implementation. |
| AC4 | TODO | Pending tests. |
| AC5 | TODO | Pending validation. |
Risks and Trade-offs
- Overflow behavior differs above
u64::MAX. The old path saturated throughf64, andsum_counteradds with uncheckedu64, which panics in debug and wraps in release. A counter that reachesu64::MAXis unreachable at any realistic request rate, since it would need about 580,000 years at one million requests per second. The doc comment on the new calls should say so, and the UDP server code should not claim saturation. - A partial bump breaks the build, or worse, builds with two copies of the crate. Mitigation: bump all eight manifests in one commit and assert a single resolved version in AC1.
- Transitive dependency changes in
Cargo.lock. Mitigation: review the lock diff and record any change that is nottorrust-metricsitself. - The gauge cast stays. It is the cleanest outcome for a true gauge and is documented as permanent, so it is not a deferred cleanup.
Implementation Completion Review
- Retrospective:
Not yet assessed - Create
implementation-retrospective.mdfromdocs/templates/IMPLEMENTATION-RETROSPECTIVE.mdin this folder if the work invalidates an assumption, changes design materially, or yields a reusable lesson; otherwise add a progress-log entry stating why none is needed.
References
- Related issues: Review Metric Aggregate Conversions #2244, Review Numeric Conversion Boundaries #2243, Add an exact u64 sum aggregate for counters torrust-metrics#1
- Related PRs: fix(udp-server): [#2244] review metric aggregate conversions and fix the UDP average processing time #2552 (the tracker side of the round trip), feat: add SumCounter for exact u64 counter sums torrust-metrics#2 (the new API)
- Release: https://github.com/torrust/torrust-metrics/releases/tag/v0.2.0
- Related ADRs: None
Activity
test(udp-server): [torrust#2560] use a fixed timestamp for the exact-…
refactor(statistics): [torrust#2560] share the counter read behind a …
docs(issues): [torrust#2560] set the spec in review and rerun the evi…
docs(issues): [torrust#2560] make the Background say where the gauge …
docs(issues): [torrust#2560] keep the independent-reviewer-reports ch…
docs(pr-reviews): [torrust#2560] add the PR torrust#2561 review audit
docs(statistics): [torrust#2560] say that counter_total reads a gauge…
docs(pr-reviews): [torrust#2560] record the third review round in the…
docs(issues): [torrust#2560] correct the overflow claim: only the f64…
docs(pr-reviews): [torrust#2560] record Copilot's second review in th…
docs(issues): [torrust#2560] put the overflow check where a reader lo…
docs(pr-reviews): [torrust#2560] record the fourth review round in th…
docs(issues): [#2560] add the spec to adopt the exact counter sum fro…
chore(deps): [#2560] bump torrust-metrics to 0.2.0
refactor(http-core): [#2560] read counter totals through sum_counter
refactor(udp-core): [#2560] read counter totals through sum_counter
refactor(udp-server): [#2560] read counter totals through sum_counter
docs(issues): [#2560] record the verification evidence and the comple…
chore(issues): archive closed issue #2560 spec to docs/issues/closed
Merge #2573: chore(issues): archive closed issue #2560 spec to docs/i…
docs(issues): [#1349] re-measure the http-core baseline before slice S2
docs(issues): [torrust#2243] record that torrust#2560 adopted the met…
Merge #2579: docs(issues): record that #2560 adopted the metrics foll…
Merge torrust#2561: refactor(statistics): [torrust#2560] adopt the ex…