diff --git a/core/configs/src/server_config/cluster.rs b/core/configs/src/server_config/cluster.rs index bf0c2cf0f6..754f596cfd 100644 --- a/core/configs/src/server_config/cluster.rs +++ b/core/configs/src/server_config/cluster.rs @@ -72,6 +72,12 @@ const MAX_VIEW_PROBE_ATTEMPTS: u32 = 100; /// static-asserts it equal to `shard::REPAIR_CHUNK_MAX`. pub const DEFAULT_REPAIR_CHUNK_MAX: usize = 128; +/// Maximum number of replicas supported by the VSR implementation. +/// +/// Duplicated here so `core/configs` does not depend on `core/consensus`; +/// `core/server` pins the values together at compile time. +pub const MAX_CLUSTER_REPLICAS: usize = 32; + /// `size_of::()`. Duplicated here for the same reason as /// [`DEFAULT_REPAIR_CHUNK_MAX`]; `core/server`'s bootstrap static-asserts it /// against the real header. Used to reject a `[message_bus] max_message_size` @@ -1227,6 +1233,14 @@ impl Validatable for ClusterConfig { return Err(ConfigurationError::InvalidConfigurationValue); } + if self.nodes.len() > MAX_CLUSTER_REPLICAS { + eprintln!( + "Invalid cluster configuration: cluster.nodes contains {} replicas, but the VSR implementation supports at most {MAX_CLUSTER_REPLICAS}", + self.nodes.len() + ); + return Err(ConfigurationError::InvalidConfigurationValue); + } + // VSR needs every replica to have a stable, unique id strictly // less than the total replica count. Duplicate ids would split the // cluster into two replicas claiming the same slot; out-of-range @@ -2467,6 +2481,18 @@ mod cluster_validate_tests { assert!(c.validate().is_ok()); } + #[test] + fn validate_accepts_the_vsr_replica_limit() { + let nodes = (0..32).map(|id| node(&format!("n{id}"), id)).collect(); + assert!(cfg(nodes).validate().is_ok()); + } + + #[test] + fn validate_rejects_rosters_above_the_vsr_replica_limit() { + let nodes = (0..33).map(|id| node(&format!("n{id}"), id)).collect(); + assert!(cfg(nodes).validate().is_err()); + } + #[test] fn validate_skips_checks_when_disabled() { let mut c = cfg(vec![]); diff --git a/core/server/config.toml b/core/server/config.toml index d199b5263b..ac9dfb700f 100644 --- a/core/server/config.toml +++ b/core/server/config.toml @@ -686,6 +686,8 @@ skip_shard_zero_for_clients = false # node's identity is resolved at launch from the '--replica-id ' CLI # flag, which selects the entry in this list that describes the current # node. All other entries are remote peers. +# VSR currently supports at most 32 nodes in this roster; larger rosters +# are rejected during server startup. # # 'ip' is the node's roster address. Replica-to-replica traffic and # follower-to-primary HTTP forwarding use it. It is not the bind interface for diff --git a/core/server/src/boot/recovery.rs b/core/server/src/boot/recovery.rs index 2ee202658e..11a4d37cee 100644 --- a/core/server/src/boot/recovery.rs +++ b/core/server/src/boot/recovery.rs @@ -485,6 +485,7 @@ const _: () = assert!( ); const _: () = assert!(configs::cluster::DEFAULT_VIEW_PROBE_ATTEMPTS_MAX == consensus::PROBE_ATTEMPTS_MAX); +const _: () = assert!(configs::cluster::MAX_CLUSTER_REPLICAS == consensus::REPLICAS_MAX); const _: () = assert!(configs::partition::DEFAULT_EVICTED_RING_CAPACITY == partitions::EVICTED_RING_CAPACITY); const _: () = assert!(