Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/bin/relay/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ prefix = ".stats" # Broadcasts appear under <prefix>/node/<no
interval = 1 # Seconds between snapshots.
node = "sjc/1" # Disambiguates relays sharing a cluster.
depth = 1 # Also bucket by the first N path segments (per tenant).
linger = "5m" # Keep an empty group's broadcast announced this long. Default.
```

Each node publishes `publisher.json`, `subscriber.json`, and `sessions.json`
Expand Down
8 changes: 6 additions & 2 deletions doc/concept/stats.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,12 @@ generate more stats.
group segment literally named `node` is ambiguous; don't use one.

At depth 0 the broadcast stays announced for the producer's life. At depth
1 or more, a group's broadcast is announced while that group has entries and
unannounced once it has none. Group numbers keep increasing across recreated
1 or more, a group's broadcast is announced while that group has entries, and
for a linger (five minutes by default) after its last one leaves. A group that
returns within the linger keeps its broadcast, so viewer churn doesn't
unannounce and re-announce it across the mesh; while it lingers empty, its
tracks hold `{}`. Once the linger elapses with the group still empty, the
broadcast is unannounced. Group numbers keep increasing across recreated
tracks, group broadcasts, and restarts; they may have gaps. A producer's first
group is the wall clock in microseconds, so a clock stepped back across a
restart parks subscribers until the new run passes their cached groups. A
Expand Down
12 changes: 11 additions & 1 deletion rs/moq-relay/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -559,14 +559,15 @@ root = ["ca.pem"]
/// Bare defaults loaded from TOML survive when the CLI does not mention them.
#[test]
fn cli_does_not_clobber_toml_stats_enabled() {
let _env = EnvGuard::clear(&["MOQ_STATS_ENABLED", "MOQ_STATS_DEPTH"]);
let _env = EnvGuard::clear(&["MOQ_STATS_ENABLED", "MOQ_STATS_DEPTH", "MOQ_STATS_LINGER"]);

let toml = r#"
[stats]
enabled = true
interval = 5
node = "localhost"
depth = 2
linger = "2m"
"#;
let dir = std::env::temp_dir().join("moq-relay-config-test");
std::fs::create_dir_all(&dir).unwrap();
Expand All @@ -583,6 +584,15 @@ depth = 2
assert_eq!(config.stats.interval, 5);
assert_eq!(config.stats.node.as_deref(), Some("localhost"));
assert_eq!(config.stats.depth, 2);
assert_eq!(config.stats.linger(), Some(std::time::Duration::from_secs(120)));

let args = vec![
std::ffi::OsString::from("moq-relay"),
std::ffi::OsString::from(&path),
std::ffi::OsString::from("--stats-linger=30s"),
];
let config = Config::parse_and_merge(args).expect("config load");
assert_eq!(config.stats.linger(), Some(std::time::Duration::from_secs(30)));
}

/// Bare runtime defaults loaded from TOML survive when the CLI omits them.
Expand Down
3 changes: 3 additions & 0 deletions rs/moq-relay/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@ struct Stats {

#[usage(env = "MOQ_STATS_DEPTH", cli("--stats-depth"))]
depth: Option<u64>,

#[usage(env = "MOQ_STATS_LINGER", cli("--stats-linger"))]
linger: Option<String>,
}

#[derive(usage::Config)]
Expand Down
28 changes: 26 additions & 2 deletions rs/moq-relay/src/stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,19 @@ pub struct Config {
setting = "stats.depth"
)]
pub depth: usize,

/// How long a group's stats broadcast stays announced after the group's
/// last session and traffic leave, e.g. "5m" or "30s". Defaults to 5
/// minutes. A group that returns within it keeps its broadcast, so viewer
/// churn doesn't unannounce and re-announce it across the mesh. Only applies
/// at `depth` 1 or more. See [`moq_stats::produce::Config::linger`].
#[usage(skip)]
#[serde(with = "crate::duration::serde_option")]
pub linger: Option<Duration>,

#[usage(long = "stats-linger", env = "MOQ_STATS_LINGER", setting = "stats.linger")]
#[serde(default, rename = "__cli_linger", skip_serializing_if = "Option::is_none")]
pub(crate) linger_arg: Option<crate::duration::Duration>,
}

impl Default for Config {
Expand All @@ -87,11 +100,18 @@ impl Default for Config {
interval: 1,
node: None,
depth: 0,
linger: None,
linger_arg: None,
}
}
}

impl Config {
/// The linger after command-line overrides; `None` keeps the producer's default.
pub(crate) fn linger(&self) -> Option<Duration> {
self.linger_arg.map(crate::duration::Duration::into_std).or(self.linger)
}

/// Build a [`moq_stats::Producer`] from this config, publishing on `origin`.
///
/// Returns a no-op producer when [`Self::enabled`] is false, so the relay can
Expand All @@ -107,13 +127,17 @@ impl Config {
let interval = Duration::from_secs(self.interval.max(1));
let node = self.node.clone().map(PathOwned::from);
let depth = self.depth;
tracing::info!(prefix, interval_secs = interval.as_secs(), node = ?node, depth, "stats publishing enabled");
let config = moq_stats::produce::Config::new()
let linger = self.linger();
tracing::info!(prefix, interval_secs = interval.as_secs(), node = ?node, depth, ?linger, "stats publishing enabled");
let mut config = moq_stats::produce::Config::new()
.with_origin(origin)
.with_prefix(prefix)
.with_interval(interval)
.with_node(node)
.with_depth(depth);
if let Some(linger) = linger {
config = config.with_linger(linger);
}
moq_stats::Producer::new(config)
}
}
Loading
Loading