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
8 changes: 8 additions & 0 deletions doc/setup/upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ error lists and rerun.

These land with the next breaking release, not the 2026-09-23 train.

- **moq-tokio config durations are optional.** Wrap durations in `Some(...)` for
connect `race`, `resolution_delay`, and `timeout`; listen `timeout`; QUIC
`idle_timeout` and `keep_alive`; backoff `initial`, `max`, and `timeout`;
GOAWAY `handover`; and WebSocket `delay`. `None` uses the default, while
`Some(Duration::ZERO)` keeps each knob's zero behavior. Values set in code
now take effect on parsed and merged configs unless the flag or environment
variable was actually given. TOML keys, C ABI, binding records, and wire
formats are unchanged.
- **A QUIC load-balancer nonce needs its server id.** `listen.lb_nonce`,
`--listen-quic-lb-nonce`, and `MOQ_LISTEN_QUIC_LB_NONCE` without `lb_id` stop
startup. Setting `lb_id` (or `lb_nonce`) and `load_balancer` together also
Expand Down
1 change: 0 additions & 1 deletion quest/m1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,6 @@ above them.
- [A/V clock](/quest/m1/av-clock.md) - the audio playhead drives Sync.reference while audio plays, through per-track sync handles
- [Plan: watch worker](/quest/m1/plan-watch-worker.md) - prototype an invisible page worker against app-spawned workers, and land the jank harness that decides
- [Watch worker](/quest/m1/watch-worker.md) - watch playback runs in a worker onto an OffscreenCanvas, so main-thread jank never stalls video or audio
- [Duration parser defaults](/quest/m1/cli-duration-defaults.md) - a moq-tokio duration set in code beats the parser default on a parsed or merged config, zero included, after #5350
- [Publish channel count](/quest/m1/publish-audio-channel-count.md) - forcing a channel count on an Audio.Capture stops costing the subscriber gaps of silence, and the media fixture's cold-start audio-tone flake is fixed at its cause
- [JS request deadline](/quest/m1/js-request-deadline.md) - each JS request, PUBLISH_NAMESPACE included, has one fatal 10 s timer from create to answer and fails fast without stream credit, so nothing queues or retries
- [Handshake deadline covers admission](/quest/m1/handshake-admission-deadline.md) - `listen.timeout` bounds a connection through auth admission on both listeners, replacing the hardcoded admission timeout
Expand Down
52 changes: 0 additions & 52 deletions quest/m1/cli-duration-defaults.md

This file was deleted.

4 changes: 2 additions & 2 deletions rs/moq-c/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1110,11 +1110,11 @@ pub extern "C" fn moq_client_defaults() -> moq_client_config {
dst.websocket_delay_us = micros(websocket.delay);
dst.has_websocket_delay = true;

dst.backoff_initial_us = micros(config.connect.backoff.initial);
dst.backoff_initial_us = micros(config.connect.backoff.initial.unwrap());
dst.has_backoff_initial = true;
dst.backoff_multiplier = config.connect.backoff.multiplier;
dst.has_backoff_multiplier = true;
dst.backoff_max_us = micros(config.connect.backoff.max);
dst.backoff_max_us = micros(config.connect.backoff.max.unwrap());
dst.has_backoff_max = true;
if let Some(timeout) = config.connect.backoff.timeout {
dst.backoff_timeout_us = micros(timeout);
Expand Down
16 changes: 8 additions & 8 deletions rs/moq-c/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,19 @@ pub unsafe fn parse_client(config: Option<&moq_client_config>) -> Result<Config,
out.connect.bind = Some(addr);
}
if config.has_connect_timeout {
out.connect.timeout = std::time::Duration::from_micros(config.connect_timeout_us);
out.connect.timeout = Some(std::time::Duration::from_micros(config.connect_timeout_us));
}
if config.has_failover_delay {
out.connect.race = std::time::Duration::from_micros(config.failover_delay_us);
out.connect.race = Some(std::time::Duration::from_micros(config.failover_delay_us));
}
if config.has_resolution_delay {
out.connect.resolution_delay = std::time::Duration::from_micros(config.resolution_delay_us);
out.connect.resolution_delay = Some(std::time::Duration::from_micros(config.resolution_delay_us));
}
if config.has_websocket_enabled {
out.connect.websocket.enabled = Some(config.websocket_enabled);
}
if config.has_websocket_delay {
out.connect.websocket.delay = std::time::Duration::from_micros(config.websocket_delay_us);
out.connect.websocket.delay = Some(std::time::Duration::from_micros(config.websocket_delay_us));
}

// TLS. `insecure` needs no flag: false is both "unset" and "verify".
Expand All @@ -87,13 +87,13 @@ pub unsafe fn parse_client(config: Option<&moq_client_config>) -> Result<Config,

// Reconnect backoff
if config.has_backoff_initial {
out.connect.backoff.initial = std::time::Duration::from_micros(config.backoff_initial_us);
out.connect.backoff.initial = Some(std::time::Duration::from_micros(config.backoff_initial_us));
}
if config.has_backoff_multiplier {
out.connect.backoff.multiplier = config.backoff_multiplier;
}
if config.has_backoff_max {
out.connect.backoff.max = std::time::Duration::from_micros(config.backoff_max_us);
out.connect.backoff.max = Some(std::time::Duration::from_micros(config.backoff_max_us));
}
if config.has_backoff_timeout {
out.connect.backoff.timeout = Some(std::time::Duration::from_micros(config.backoff_timeout_us));
Expand All @@ -104,10 +104,10 @@ pub unsafe fn parse_client(config: Option<&moq_client_config>) -> Result<Config,
out.quic.max_streams = Some(config.quic_max_streams);
}
if config.has_quic_idle_timeout {
out.quic.idle_timeout = std::time::Duration::from_micros(config.quic_idle_timeout_us);
out.quic.idle_timeout = Some(std::time::Duration::from_micros(config.quic_idle_timeout_us));
}
if config.has_quic_keep_alive {
out.quic.keep_alive = std::time::Duration::from_micros(config.quic_keep_alive_us);
out.quic.keep_alive = Some(std::time::Duration::from_micros(config.quic_keep_alive_us));
}
if config.has_quic_gso {
out.quic.gso = Some(config.quic_gso);
Expand Down
20 changes: 13 additions & 7 deletions rs/moq-c/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4258,12 +4258,15 @@ fn defaults_report_what_a_zeroed_config_dials() {
assert!(config.has_backoff_initial);
assert_eq!(
config.backoff_initial_us,
expected.connect.backoff.initial.as_micros() as u64
expected.connect.backoff.initial.unwrap().as_micros() as u64
);
assert!(config.has_backoff_multiplier);
assert_eq!(config.backoff_multiplier, expected.connect.backoff.multiplier);
assert!(config.has_backoff_max);
assert_eq!(config.backoff_max_us, expected.connect.backoff.max.as_micros() as u64);
assert_eq!(
config.backoff_max_us,
expected.connect.backoff.max.unwrap().as_micros() as u64
);
assert!(config.has_backoff_timeout);
assert_eq!(
expected.connect.backoff.timeout,
Expand Down Expand Up @@ -4314,12 +4317,12 @@ fn zero_with_a_flag_set_is_a_real_value() {

let explicit = parsed(&config);
assert_eq!(explicit.connect.backoff.timeout, Some(std::time::Duration::ZERO));
assert_eq!(explicit.quic.keep_alive, std::time::Duration::ZERO);
assert_eq!(explicit.quic.keep_alive, Some(std::time::Duration::ZERO));

// Without the flags the same zeroes mean nothing at all.
let defaults = parsed(&client_config());
assert_ne!(defaults.connect.backoff.timeout, Some(std::time::Duration::ZERO));
assert_eq!(defaults.quic.keep_alive, std::time::Duration::from_secs(3));
assert_eq!(defaults.quic.keep_alive, Some(std::time::Duration::from_secs(3)));
}

#[test]
Expand Down Expand Up @@ -4446,12 +4449,15 @@ fn config_quic_and_backoff_knobs_apply() {
let parsed = parsed(&config);
assert_eq!(
parsed.connect.backoff.initial,
std::time::Duration::from_micros(500_000)
Some(std::time::Duration::from_micros(500_000))
);
assert_eq!(parsed.connect.backoff.multiplier, 3);
assert_eq!(parsed.connect.backoff.max, std::time::Duration::from_micros(10_000_000));
assert_eq!(
parsed.connect.backoff.max,
Some(std::time::Duration::from_micros(10_000_000))
);
assert_eq!(parsed.quic.max_streams, Some(4096));
assert_eq!(parsed.quic.idle_timeout, std::time::Duration::from_millis(15_000));
assert_eq!(parsed.quic.idle_timeout, Some(std::time::Duration::from_millis(15_000)));
assert_eq!(parsed.quic.gso, Some(false));
assert_eq!(parsed.quic.mtu_discovery, Some(true));
assert_eq!(parsed.quic.qlog.as_deref(), Some(std::path::Path::new(dir)));
Expand Down
6 changes: 3 additions & 3 deletions rs/moq-ffi/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,20 +165,20 @@ impl Client {

connect.websocket.enabled = config.websocket.enabled;
if let Some(delay) = config.websocket.delay_us {
connect.websocket.delay = std::time::Duration::from_micros(delay);
connect.websocket.delay = Some(std::time::Duration::from_micros(delay));
}

connect.once = Some(config.once);

let backoff = config.backoff;
if let Some(initial) = backoff.initial_us {
connect.backoff.initial = std::time::Duration::from_micros(initial);
connect.backoff.initial = Some(std::time::Duration::from_micros(initial));
}
if let Some(multiplier) = backoff.multiplier {
connect.backoff.multiplier = multiplier;
}
if let Some(max) = backoff.max_us {
connect.backoff.max = std::time::Duration::from_micros(max);
connect.backoff.max = Some(std::time::Duration::from_micros(max));
}
if let Some(timeout) = backoff.timeout_us {
connect.backoff.timeout = Some(std::time::Duration::from_micros(timeout));
Expand Down
4 changes: 2 additions & 2 deletions rs/moq-gst/src/sink/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,10 @@ pub(super) fn quic_config(settings: &ResolvedSettings) -> moq_tokio::quic::Confi
// The properties are optional and the config fields are not: an unset property
// leaves the library default rather than overriding it with one of its own.
if let Some(idle_timeout) = settings.quic_idle_timeout {
config.idle_timeout = idle_timeout;
config.idle_timeout = Some(idle_timeout);
}
if let Some(keep_alive) = settings.quic_keep_alive {
config.keep_alive = keep_alive;
config.keep_alive = Some(keep_alive);
}
config
}
Expand Down
2 changes: 1 addition & 1 deletion rs/moq-relay/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -920,7 +920,7 @@ timeout = "2m"
let args = vec![std::ffi::OsString::from("moq-relay"), std::ffi::OsString::from(&path)];
let config = Config::parse_and_merge(args).expect("config load");

assert_eq!(config.connect.timeout, std::time::Duration::from_secs(120));
assert_eq!(config.connect.timeout, Some(std::time::Duration::from_secs(120)));
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion rs/moq-relay/src/shutdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ mod tests {
async fn withheld_websocket_setup_ends_at_the_deadline() {
let window = Duration::from_secs(1);
let mut config = crate::Config::default();
config.listen.timeout = Duration::ZERO;
config.listen.timeout = Some(Duration::ZERO);
config.listen.bind = Some("127.0.0.1:0".parse().unwrap());
config.listen.tls.generate = vec!["localhost".into()];
config.web.ws = true;
Expand Down
2 changes: 1 addition & 1 deletion rs/moq-relay/src/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1743,7 +1743,7 @@ mod tests {
let (ca, cert, key) = make_certs(&certs);
let mut config = crate::Config::default();
config.listen.tcp.bind = Some("127.0.0.1:0".parse().unwrap());
config.listen.timeout = std::time::Duration::ZERO;
config.listen.timeout = Some(std::time::Duration::ZERO);
config.web.ws = true;
config.auth.public = vec![moq_auth::Pattern::all()];
config.drain_timeout = DRAIN_WINDOW;
Expand Down
4 changes: 2 additions & 2 deletions rs/moq-relay/tests/auth_lifetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ fn client() -> moq_tokio::Client {
let mut config = moq_tokio::connect::Config::default();
config.tls.insecure = Some(true);
config.once = Some(true);
config.websocket.delay = Duration::ZERO;
config.websocket.delay = Some(Duration::ZERO);
config.bind = Some("127.0.0.1:0".parse().expect("parse bind"));
config.init(Default::default()).expect("client init")
}
Expand Down Expand Up @@ -1421,7 +1421,7 @@ async fn in_band_tokens_widen_and_withdraw_live_prefixes() {
let mut config = moq_tokio::connect::Config::default();
config.once = Some(true);
config.version = vec!["moq-transport-22".parse().unwrap()];
config.websocket.delay = Duration::ZERO;
config.websocket.delay = Some(Duration::ZERO);
config.init(Default::default()).unwrap()
};
let origin = moq_tokio::origin::spawn();
Expand Down
2 changes: 1 addition & 1 deletion rs/moq-relay/tests/cluster_unknown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ async fn spawn_relay(
fn client(version: Option<moq_net::Version>) -> moq_tokio::Client {
let mut config = moq_tokio::connect::Config::default();
config.tls.insecure = Some(true);
config.websocket.delay = Duration::ZERO;
config.websocket.delay = Some(Duration::ZERO);
config.bind = Some("127.0.0.1:0".parse().expect("parse bind"));
config.version.extend(version);
config.init(Default::default()).expect("client init")
Expand Down
10 changes: 5 additions & 5 deletions rs/moq-relay/tests/drills.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,8 @@ fn quic() -> moq_tokio::quic::Config {
// busy runner has to stall eight times over before a live session is mistaken
// for a dead one.
let mut quic = moq_tokio::quic::Config::default();
quic.idle_timeout = Duration::from_secs(2);
quic.keep_alive = Duration::from_millis(250);
quic.idle_timeout = Some(Duration::from_secs(2));
quic.keep_alive = Some(Duration::from_millis(250));
quic
}

Expand All @@ -327,8 +327,8 @@ fn dial() -> moq_tokio::connect::Config {

// Fast enough to keep a relay bounce or a link flap inside the drill's
// budget, paced enough that the loop is still a backoff.
config.backoff.initial = Duration::from_millis(50);
config.backoff.max = Duration::from_millis(200);
config.backoff.initial = Some(Duration::from_millis(50));
config.backoff.max = Some(Duration::from_millis(200));
config.backoff.timeout = Some(Duration::from_secs(5));
config
}
Expand Down Expand Up @@ -981,7 +981,7 @@ async fn cross_cluster(lane: Lane, link: PeerLink) {
// idles out only after `relay_idle`, which the flap's waits allow for.
let mut config = relay_config();
config.quic = credit(lane, config.quic);
let relay_idle = config.quic.idle_timeout;
let relay_idle = config.quic.resolve().idle_timeout;
let origin = RelayHost::start(config).await;
let seed = seed(lane);
let peer = match (link, lane) {
Expand Down
14 changes: 7 additions & 7 deletions rs/moq-relay/tests/goaway_cluster.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ async fn cluster_continues_a_group_split_by_goaway_inner() {

let mut client_config = moq_tokio::connect::Config::default();
client_config.tls.insecure = Some(true);
client_config.goaway.handover = Duration::from_secs(2);
client_config.goaway.handover = Some(Duration::from_secs(2));
let client = client_config.init(Default::default()).expect("client init");

let mut cluster_config = cluster::Config::default();
Expand Down Expand Up @@ -294,7 +294,7 @@ async fn cluster_migrates_on_upstream_goaway_inner() {
let mut client_config = moq_tokio::connect::Config::default();
client_config.tls.insecure = Some(true);
// Short handover so the test observes the old session close quickly.
client_config.goaway.handover = Duration::from_secs(2);
client_config.goaway.handover = Some(Duration::from_secs(2));
let client = client_config.init(Default::default()).expect("client init");

let mut cluster_config = cluster::Config::default();
Expand Down Expand Up @@ -419,7 +419,7 @@ async fn spawn_relay_with_upstream(
let mut client_config = moq_tokio::connect::Config::default();
client_config.tls.insecure = Some(true);
// Short handover so the test observes the old session close quickly.
client_config.goaway.handover = Duration::from_secs(2);
client_config.goaway.handover = Some(Duration::from_secs(2));
// Only lite-07 carries the epoch that lets a failover resume the subscription.
client_config.version = vec![LITE_07.parse().unwrap()];
let client = client_config.init(Default::default()).expect("client init");
Expand Down Expand Up @@ -503,7 +503,7 @@ async fn cluster_diamond_goaway_seamless_failover_inner() {
let mut client_config = moq_tokio::connect::Config::default();
client_config.tls.insecure = Some(true);
// Short handover so the test observes the old session close quickly.
client_config.goaway.handover = Duration::from_secs(2);
client_config.goaway.handover = Some(Duration::from_secs(2));
client_config.version = vec![LITE_07.parse().unwrap()];
let mid_a_client = client_config.init(Default::default()).expect("mid-a client init");
let (_mid_a_upstream_client, mid_a_upstream) = within(
Expand Down Expand Up @@ -768,7 +768,7 @@ async fn cluster_reconnects_on_empty_uri_goaway_inner() {
let mut client_config = moq_tokio::connect::Config::default();
client_config.tls.insecure = Some(true);
// Short handover so the test observes the old session close quickly.
client_config.goaway.handover = Duration::from_secs(2);
client_config.goaway.handover = Some(Duration::from_secs(2));
// Only lite-07 carries the epoch that lets the redial resume the subscription.
client_config.version = vec![LITE_07.parse().unwrap()];
let client = client_config.init(Default::default()).expect("client init");
Expand Down Expand Up @@ -895,11 +895,11 @@ async fn goaway_handover_is_enforced_while_the_replacement_dial_hangs_inner() {
let handover = Duration::from_millis(200);
let mut client_config = moq_tokio::connect::Config::default();
client_config.tls.insecure = Some(true);
client_config.goaway.handover = handover;
client_config.goaway.handover = Some(handover);
// The GOAWAY has to land on a *healthy* session, which is the path that goes
// straight into the replacement dial. Below this bar it takes the immediate
// redirect path instead, whose sleep polls the drain either way.
client_config.backoff.initial = Duration::from_millis(50);
client_config.backoff.initial = Some(Duration::from_millis(50));
let client = client_config.init(Default::default()).expect("client init");

let url: Url = format!("tcp://127.0.0.1:{port}/").parse().expect("parse url");
Expand Down
2 changes: 1 addition & 1 deletion rs/moq-relay/tests/public_rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const TIMEOUT: Duration = Duration::from_secs(10);
fn client() -> moq_tokio::Client {
let mut config = moq_tokio::connect::Config::default();
config.once = Some(true);
config.websocket.delay = Duration::ZERO;
config.websocket.delay = Some(Duration::ZERO);
config.bind = Some("127.0.0.1:0".parse().expect("parse bind"));
config.init(Default::default()).expect("client init")
}
Expand Down
Loading
Loading