From 49b28efcbe252af5be2a9694171e9b3d588882a6 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 4 Oct 2026 21:27:06 -0700 Subject: [PATCH 1/2] quest: claim m1/cluster-shims Co-Authored-By: Claude Opus 5.5 From 2f525f0e0480ce01ded6216428d87aa881589e23 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 4 Oct 2026 21:37:28 -0700 Subject: [PATCH 2/2] feat(relay)!: delete the removed cluster mesh and linger flags cluster::Config drops the hidden mesh, linger, and linger_arg fields that only existed to refuse --cluster-mesh and --cluster-linger by name. Both refusals have shipped (linger in 0.15.0, mesh in 0.17.0), so the flags and TOML keys are now unknown like any other. deprecated() keeps the bare-host --cluster-connect refusal. Co-Authored-By: Claude Opus 5.5 --- doc/setup/upgrade.md | 5 ++ quest/m1/README.md | 1 - quest/m1/cluster-shims.md | 17 ----- rs/moq-cli/src/args.rs | 6 -- rs/moq-relay/README.md | 2 +- rs/moq-relay/src/cache.rs | 6 +- rs/moq-relay/src/cluster.rs | 117 +---------------------------- rs/moq-relay/src/config.rs | 19 ----- rs/moq-relay/src/settings.rs | 6 -- rs/moq-relay/tests/released_cli.rs | 2 - 10 files changed, 12 insertions(+), 169 deletions(-) delete mode 100644 quest/m1/cluster-shims.md diff --git a/doc/setup/upgrade.md b/doc/setup/upgrade.md index ac775aaace..08cf4999b9 100644 --- a/doc/setup/upgrade.md +++ b/doc/setup/upgrade.md @@ -41,6 +41,11 @@ These land with the next breaking release, not the 2026-09-23 train. `SourceMap` (#4667) are gone, along with the importers' `live()`. Publish the source's own timestamps and let the catalog clock map them to wall time; pin that mapping with `Config::with_clock` when the source's zero is known. +- **`--cluster-mesh` and `--cluster-linger` are unknown flags.** moq-relay + 0.17 refuses them by name; later relays reject them, and TOML `mesh` and + `linger`, like any unknown setting. `MOQ_CLUSTER_MESH` and + `MOQ_CLUSTER_LINGER` are no longer read, so drop them from the environment. + In Rust, `cluster::Config` has no `mesh` or `linger` field. ## Wire diff --git a/quest/m1/README.md b/quest/m1/README.md index 42316c2401..3532c8b0f5 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -27,7 +27,6 @@ QUIC studies there on that rule. - [Cluster routing](/quest/m1/cluster-routing/README.md) - any node routes toward a broadcast's origin over CDN and P2P links alike, with per-origin routes and path-less announces - [Transcoders start at group boundaries](/quest/m1/transcode-group-start.md) - moq-transcode serves from a group boundary and mirrors its source's sequences, so two instances are interchangeable - [One route cost](/quest/m1/route-cost.md) - Warm and Cold collapse to one static route cost -- [Delete removed cluster flags](/quest/m1/cluster-shims.md) - `mesh` and `linger` leave `cluster::Config` and their flags become unknown - [Track tail interop](/quest/m1/track-tail-interop.md) - a Rust publisher ending a track with a group in flight is read to its end by the JS subscriber, and the reverse, in `just test interop` - [JS close drain](/quest/m1/js-close-drain.md) - a JS `close()` waits for served subscriptions, and on lite-07 for the subscriber FIN, as Rust does - [Demand everywhere](/quest/m1/demand-everywhere.md) - requests and producers at every level watch subscribers through `demand()` alone, ahead of the bindings exposing it diff --git a/quest/m1/cluster-shims.md b/quest/m1/cluster-shims.md deleted file mode 100644 index 8a8beb8180..0000000000 --- a/quest/m1/cluster-shims.md +++ /dev/null @@ -1,17 +0,0 @@ -# [XS] Delete the removed cluster flags - -## Goal - -`cluster::Config` no longer carries the hidden `mesh` and `linger` fields, -and `--cluster-mesh` and `--cluster-linger` are unknown flags like any other. - -## Plan - -Both fields exist only to refuse their removed settings at startup with a -pointer to the replacement (`deprecated()` in `rs/moq-relay/src/cluster.rs`, -checked there and in `config.rs`). The linger refusal shipped in -moq-relay 0.15.0 and the mesh refusal (#4601) in 0.17.0, so delete the -fields, `deprecated()`, and its checks, per the no-shim rule. Removing public -fields is a break. - -Public API: removes two hidden `cluster::Config` fields. Wire: none. diff --git a/rs/moq-cli/src/args.rs b/rs/moq-cli/src/args.rs index 4379abc173..d0720c71d0 100644 --- a/rs/moq-cli/src/args.rs +++ b/rs/moq-cli/src/args.rs @@ -1608,12 +1608,6 @@ mod tests { assert!(err.contains(reported), "{err}"); } - // Gossip discovery is removed, so its flag is refused for every verb. - let Err(err) = Invocation::try_parse_from(["moq", "--cluster-mesh", "auth", "generate"]) else { - panic!("--cluster-mesh must be refused"); - }; - assert!(err.to_string().contains("--cluster-mesh"), "{err}"); - #[cfg(unix)] { for (flag, value, reported) in [ diff --git a/rs/moq-relay/README.md b/rs/moq-relay/README.md index 88edcfac41..692d714189 100644 --- a/rs/moq-relay/README.md +++ b/rs/moq-relay/README.md @@ -64,7 +64,7 @@ Relays can be joined together to proxy announcements and subscriptions. A viewer - `--cluster-connect-api ` fetches that list from an endpoint or file instead, reloading it without a restart. - `--cluster-lan` finds peers on the local network over mDNS. -A relay only dials peers listed this way or found on the LAN. `--cluster-mesh` gossip discovery was removed; the relay errors at startup if it is set. +A relay only dials peers listed this way or found on the LAN. See [doc/bin/relay/cluster.md](https://github.com/moq-dev/moq/blob/main/doc/bin/relay/cluster.md) for the full walkthrough, including topology trade-offs and authentication. diff --git a/rs/moq-relay/src/cache.rs b/rs/moq-relay/src/cache.rs index 8c476f034e..7926cd0819 100644 --- a/rs/moq-relay/src/cache.rs +++ b/rs/moq-relay/src/cache.rs @@ -308,8 +308,10 @@ mod tests { let before = spawned(); // `--cluster-id 0` is rejected, so the cache is dropped before it is attached. - let mut cluster = crate::cluster::Config::default(); - cluster.id = Some(0); + let cluster = crate::cluster::Config { + id: Some(0), + ..Default::default() + }; assert!(attach(&governed(), cluster).is_err(), "cluster id 0 is rejected"); settle().await; diff --git a/rs/moq-relay/src/cluster.rs b/rs/moq-relay/src/cluster.rs index 1c4c78f70b..ac4b22ccca 100644 --- a/rs/moq-relay/src/cluster.rs +++ b/rs/moq-relay/src/cluster.rs @@ -609,23 +609,6 @@ pub struct Config { )] pub node: Option, - /// Released spelling of the removed gossip discovery, kept so - /// [`Self::deprecated`] can refuse it. Any value, boolean or the older URL - /// form, is refused. - #[doc(hidden)] - #[usage( - name = "cluster-mesh", - long = "cluster-mesh", - env = "MOQ_CLUSTER_MESH", - setting = "cluster.mesh", - default_missing = "true", - num_args = 0..=1, - require_equals = true, - hide = true, - )] - #[serde(default, deserialize_with = "deserialize_bool_or_string")] - pub mesh: Option, - /// LAN discovery over mDNS (`[cluster.lan]`). #[cfg(feature = "cluster-lan")] #[usage(flatten)] @@ -654,43 +637,12 @@ pub struct Config { setting = "cluster.tier" )] pub tier: Option, - /// Released spelling, kept so [`Self::deprecated`] can name that linger is gone. - #[doc(hidden)] - #[usage(skip)] - #[serde(with = "crate::duration::serde_option")] - pub linger: Option, - - #[usage( - name = "cluster-linger", - long = "cluster-linger", - env = "MOQ_CLUSTER_LINGER", - setting = "cluster.linger", - hide = true - )] - #[serde(default, rename = "__cli_linger", skip_serializing_if = "Option::is_none")] - linger_arg: Option, } impl Config { /// Released spellings this config was parsed from, each paired with what replaced it. pub fn deprecated(&self) -> moq_tokio::cli::Deprecated { let mut found = moq_tokio::cli::Deprecated::default(); - if self.linger.is_some() || self.linger_arg.is_some() { - found.changed( - "--cluster-linger", - Some("MOQ_CLUSTER_LINGER"), - "(removed)", - "a broadcast closes as soon as its last publisher is lost", - ); - } - if self.mesh.is_some() { - found.changed( - "--cluster-mesh", - Some("MOQ_CLUSTER_MESH"), - "--cluster-connect or --cluster-connect-api", - "gossip discovery is removed; list every peer this relay dials", - ); - } if self.connect.iter().any(|peer| is_legacy_peer(peer.url())) { found.changed( "--cluster-connect", @@ -1961,31 +1913,6 @@ pub(crate) fn canonicalize_peer_key(peer: &str) -> String { } } -/// Deserialize a field that accepts either a TOML boolean or string into an -/// `Option` (booleans become `"true"` / `"false"`). Lets the removed -/// `cluster.mesh` parse in both its released forms, `mesh = true` and -/// `mesh = ""`, so it is refused by name rather than as an unknown type. -fn deserialize_bool_or_string<'de, D>(deserializer: D) -> Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - use serde::Deserialize as _; - - #[derive(serde::Deserialize)] - #[serde(untagged)] - enum BoolOrString { - Bool(bool), - Str(String), - } - - Ok( - Option::::deserialize(deserializer)?.map(|value| match value { - BoolOrString::Bool(value) => value.to_string(), - BoolOrString::Str(value) => value, - }), - ) -} - #[cfg(test)] mod tests { use super::*; @@ -2582,24 +2509,6 @@ mod tests { task.abort(); } - /// Gossip discovery is removed, so every released `--cluster-mesh` form, the - /// boolean and the older self-URL, stops construction and names the - /// replacement instead of leaving a relay without the peers it expected. - #[tokio::test] - async fn removed_mesh_is_refused() { - for value in ["true", "false", "rendezvous.example.com:4443"] { - let err = new_cluster(Config { - mesh: Some(value.to_string()), - ..Default::default() - }) - .err() - .expect("mesh must be refused"); - let msg = format!("{err}"); - assert!(msg.contains("--cluster-mesh / MOQ_CLUSTER_MESH"), "{value}: {msg}"); - assert!(msg.contains("--cluster-connect"), "{value}: {msg}"); - } - } - /// A valid `cluster.id` is used verbatim as the relay's Hop ID, giving the /// node a stable identity across restarts. #[tokio::test] @@ -2705,26 +2614,6 @@ mod tests { ); } - /// A TOML `mesh` in either released type is refused by name at load, so a - /// config file that relied on gossip stops instead of starting without peers. - #[test] - fn toml_mesh_is_refused() { - // Usage reads the environment while parsing, so serialize with the tests - // that mutate it. - let _env = crate::test_env::EnvGuard::lock(); - - let dir = std::env::temp_dir().join("moq-relay-cluster-test"); - std::fs::create_dir_all(&dir).unwrap(); - for (name, value) in [("bool", "true"), ("url", "\"us-east.example.com:4443\"")] { - let path = dir.join(format!("cluster-mesh-{name}-toml.toml")); - std::fs::write(&path, format!("[cluster]\nmesh = {value}\n")).unwrap(); - - let args = vec![std::ffi::OsString::from("moq-relay"), std::ffi::OsString::from(&path)]; - let err = RelayConfig::parse_and_merge(args).expect_err("mesh must be refused"); - assert!(err.to_string().contains("--cluster-mesh"), "{name}: {err}"); - } - } - /// Static config accepts the object form beside bare URLs, through the same /// type the connect API parses. #[test] @@ -2778,17 +2667,15 @@ mod tests { assert!(!is_legacy_peer("https://cdn.example.com/?jwt=abc")); } - /// Linger and a bare `--cluster-connect` host still parse so the process can - /// name what replaced them, but they configure nothing and must stop a run. + /// A bare `--cluster-connect` host still parses so the process can name what + /// replaced it, but it configures nothing and must stop a run. #[test] fn released_cluster_spellings_are_reported_not_applied() { let config = Config { - linger: Some(std::time::Duration::from_secs(5)), connect: vec![Peer::new("root.example.com:4443")], ..Default::default() }; let reported = config.deprecated().to_string(); - assert!(reported.contains("--cluster-linger / MOQ_CLUSTER_LINGER"), "{reported}"); assert!( reported.contains("a full URL like https://host/?jwt=TOKEN"), "{reported}" diff --git a/rs/moq-relay/src/config.rs b/rs/moq-relay/src/config.rs index 31975a5f4f..3775b4ae88 100644 --- a/rs/moq-relay/src/config.rs +++ b/rs/moq-relay/src/config.rs @@ -694,25 +694,6 @@ duration = "30s" toml::to_string(&unset).expect("serialize None"); } - /// A released TOML value still survives the merge so the refusal can name it. - #[test] - fn cli_does_not_clobber_toml_linger() { - let _env = EnvGuard::clear(&["MOQ_CLUSTER_LINGER"]); - - let toml = r#" -[cluster] -linger = "30s" -"#; - let dir = std::env::temp_dir().join("moq-relay-config-test"); - std::fs::create_dir_all(&dir).unwrap(); - let path = dir.join("linger-toml-wins.toml"); - std::fs::write(&path, toml).unwrap(); - - let args = vec![std::ffi::OsString::from("moq-relay"), std::ffi::OsString::from(&path)]; - let err = Config::parse_and_merge(args).expect_err("must refuse").to_string(); - assert!(err.contains("--cluster-linger"), "{err}"); - } - /// Preferred addresses loaded from TOML survive when the CLI omits them. #[test] fn cli_does_not_clobber_toml_preferred_addresses() { diff --git a/rs/moq-relay/src/settings.rs b/rs/moq-relay/src/settings.rs index ad212f27c0..270b8f5b74 100644 --- a/rs/moq-relay/src/settings.rs +++ b/rs/moq-relay/src/settings.rs @@ -78,18 +78,12 @@ struct Cluster { #[usage(env = "MOQ_CLUSTER_NODE", cli("--cluster-node"))] node: Option, - #[usage(env = "MOQ_CLUSTER_MESH", cli("--cluster-mesh"))] - mesh: Option, - #[usage(env = "MOQ_CLUSTER_TOKEN", cli("--cluster-token"))] token: Option, #[usage(env = "MOQ_CLUSTER_TIER", cli("--cluster-tier"))] tier: Option, - #[usage(env = "MOQ_CLUSTER_LINGER", cli("--cluster-linger"))] - linger: Option, - #[cfg(feature = "cluster-lan")] #[usage(flatten)] lan: ClusterLan, diff --git a/rs/moq-relay/tests/released_cli.rs b/rs/moq-relay/tests/released_cli.rs index b89a8eeeb2..a942bc314c 100644 --- a/rs/moq-relay/tests/released_cli.rs +++ b/rs/moq-relay/tests/released_cli.rs @@ -55,8 +55,6 @@ const RELEASED: &[(&str, Option<&str>)] = &[ ("cluster-connect", Some("MOQ_CLUSTER_CONNECT")), ("cluster-connect-api", Some("MOQ_CLUSTER_CONNECT_API")), ("cluster-id", Some("MOQ_CLUSTER_ID")), - ("cluster-linger", Some("MOQ_CLUSTER_LINGER")), - ("cluster-mesh", Some("MOQ_CLUSTER_MESH")), ("cluster-node", Some("MOQ_CLUSTER_NODE")), ("cluster-tier", Some("MOQ_CLUSTER_TIER")), ("cluster-token", Some("MOQ_CLUSTER_TOKEN")),