moq-net(stats): take a StatsConfig value type in Stats::new#1537
Conversation
The #1517 rewrite left Stats::new taking 5 positional args (prefix, interval, retention, node, origin), two of them path-like, which trips the "4+ args is a struct waiting to happen" guideline and is easy to misorder at the call site. Replace it with Stats::new(origin) plus chained with_prefix / with_interval / with_retention / with_node setters, each defaulting to sensible values. To make the by-value `with_*(self) -> Self` setters safe, the set-once config (prefix, node, interval, retention, enabled) moves out from behind the Arc into plain Stats fields; only the genuinely shared runtime state (origin, entries, task, tick_counter) stays in Arc<StatsShared>. This avoids the Arc::get_mut "already cloned" hazard a builder would otherwise have, with no separate builder struct. advertised stops being precomputed in the Arc and is derived lazily from prefix + node at task-spawn time; disabled detection moves to an explicit enabled flag. The snapshot task takes the config it needs by value at spawn (it never re-reads config per tick). Also mark moq-net and moq-relay semver_check = false in .release-plz.toml: no external consumers yet, so the deliberate stats API break only warrants a patch bump rather than the 0.x minor release-plz would otherwise force. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Keep semver_check enabled (we want real violations caught), but take the deliberate stats API break manually this time: the stats API is new and has no external consumers, so a patch bump is enough. - moq-net 0.1.4 -> 0.1.5 - moq-relay 0.12.1 -> 0.12.2 Reverts the .release-plz.toml semver_check = false escape hatch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
WalkthroughThis pull request refactors the Stats type from a multi-parameter constructor to a builder-based API. A new public 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…ilder A by-value builder on Stats was a footgun: Stats is a cheap-clone shared handle, so calling a with_* setter after cloning or deriving tier handles would silently configure only one copy. Move the knobs into a dedicated StatsConfig value type instead. StatsConfig::new(origin) plus with_prefix / with_interval / with_retention / with_node, handed to Stats::new(config). The config is #[non_exhaustive] so new knobs can land without breaking call sites. origin lives in the config (it's required, so there's no Default). Stats::new destructures the config, normalizing an empty node to None there so a directly-assigned field is handled too. The relay refers to it as moq_net::StatsConfig to avoid colliding with its own clap-derived StatsConfig. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
rs/moq-net/src/stats.rs (1)
855-890:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winGuard against a zero
intervalsorun_publishercan’t panic
tokio::time::intervalpanics when given aDuration::ZEROperiod, so ensure the task never constructs a ticker with a zero period.🛡️ Proposed diff
- let mut ticker = tokio::time::interval(interval); + // `tokio::time::interval` panics on a zero period; fall back to 1s so a + // misconfigured `with_interval(Duration::ZERO)` can't kill the task. + let period = if interval.is_zero() { Duration::from_secs(1) } else { interval }; + let mut ticker = tokio::time::interval(period);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@rs/moq-net/src/stats.rs` around lines 855 - 890, run_publisher currently constructs a tokio::time::interval with the supplied interval which will panic if interval == Duration::ZERO; guard against that by validating/clamping the incoming interval before creating the ticker (inside run_publisher): check interval.is_zero() and either return early with a warning via tracing::warn! or replace it with a small non‑zero fallback (e.g., Duration::from_millis(1)) and then call tokio::time::interval with the safe value; update the variable used to create ticker and keep the existing ticker.set_missed_tick_behavior call.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@rs/moq-net/src/stats.rs`:
- Around line 855-890: run_publisher currently constructs a
tokio::time::interval with the supplied interval which will panic if interval ==
Duration::ZERO; guard against that by validating/clamping the incoming interval
before creating the ticker (inside run_publisher): check interval.is_zero() and
either return early with a warning via tracing::warn! or replace it with a small
non‑zero fallback (e.g., Duration::from_millis(1)) and then call
tokio::time::interval with the safe value; update the variable used to create
ticker and keep the existing ticker.set_missed_tick_behavior call.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: ae6519e3-4a0d-430b-b1ac-d8ebadf5b687
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (4)
rs/moq-net/Cargo.tomlrs/moq-net/src/stats.rsrs/moq-relay/Cargo.tomlrs/moq-relay/src/stats.rs
Reconcile main into dev. Key conflict resolutions: - conducer crate renamed to kio (main #1547): applied across all of dev's newer code; dropped the stale conducer path-dep, kept dev's new flate2 dep. - moq-mux: kept dev's thiserror Result (#1495); dropped main's CatalogSource as dead code since dev's catalog::Consumer already unifies Hang/MSF. - moq-net: kept dev's OriginConsumer/AnnounceConsumer split (#1434) and the TrackConsumer end_at cap; kept dev's non-optional auto-created origins on the lite session/publisher (#e770). - stats: combined main's StatsConfig + liveness retention (#1537, #1548) with dev's AnnounceConsumer usage. - libmoq + moq-native: kept main's auto-reconnect (#1544), terminal-callback contract (#1546), and consume_announced (#1552), adapted to dev's AnnounceConsumer and OriginProducer connect API. Restored the InitFailed error variant and made moq-rtc handle the now-fallible Log::init. cargo check/clippy/test all pass on the merged workspace.
Summary
Follow-up to #1517, which left
moq_net::Stats::newtaking 5 positional args (prefix, interval, retention, node, origin), two of them path-like and easy to misorder. This replaces it with a dedicatedStatsConfigvalue type and bumps the patch versions for the (unreleased) stats API break.What changed
StatsConfigvalue type +Stats::new(config).StatsConfig::new(origin)plus chainedwith_prefix/with_interval/with_retention/with_node, each defaulting to sensible values (.stats, 1s, retention 1, no node), handed toStats::new(config).Stats?Statsis a cheap-clone shared handle, soStats::new(origin).with_foo()style setters would be a footgun: calling one after cloning or derivingtierhandles would silently configure only one copy. A separate config value type sidesteps that entirely.#[non_exhaustive]onStatsConfigso new knobs can land without breaking call sites; construct viaStatsConfig::newrather than a struct literal.originlives in the config (it's required, so there's noDefault).prefix,node,interval,retention,enabled) lives in plainStatsfields; only genuinely shared runtime state (origin,entries,task,tick_counter) stays inArc<StatsShared>.Stats::newdestructures the config, normalizing an emptynodetoNonethere (so a directly-assigned field is handled too).advertisedis derived lazily fromprefix + nodeat task-spawn time; disabled detection is an explicitenabledflag. The snapshot task takes the config it needs by value at spawn.moq-relayStatsConfig::build), referring to it asmoq_net::StatsConfigto avoid colliding with the relay's own clap-derivedStatsConfig. Plus all stats tests.moq-net0.1.4 -> 0.1.5,moq-relay0.12.1 -> 0.12.2. The stats API is new with no external consumers, so the deliberate break is taken as a patch bump this time rather than the0.xminor semver-checks would otherwise force.semver_checkstays enabled so real violations are still caught.Reviewer notes
mainbecause the moq-net(stats): aggregate per-node into a single gzipped broadcast #1517 rewrite this builds on is already onmainand the patch release fires frommain. Per the branch-targeting guide a breakingmoq-netAPI change would normally go todev; happy to retarget if preferred, but it would then diverge from the code being released.moq-net 0.1.4), so there's no released-API impact beyond what moq-net(stats): aggregate per-node into a single gzipped broadcast #1517 already introduced.moq-net = { version = "0.1", ... }(^0.1), so 0.1.5 satisfies every dependent without touching their requirements.Test plan
cargo test -p moq-net stats::(17 passed)cargo test -p moq-relay stats(config merge tests passed)cargo clippy -p moq-net -p moq-relay --all-targets(clean)cargo check -p moq-net -p moq-relayafter version bump (Cargo.lock refreshed)(Written by Claude)