From 2eea20a216fc979cb4a379917ce627d7055060f2 Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Tue, 9 Jun 2026 23:55:48 +0000 Subject: [PATCH] tcp: drive inbound connection cap from node.limits.max_connections The per-transport TCP inbound cap was hardwired to 256 and never read node.limits.max_connections, so raising max_connections was a silent no-op for inbound TCP. Resolve the effective cap with precedence: explicit per-transport max_inbound_connections, then node-wide max_connections, then the built-in default of 256. Established peers remain bounded node-wide by add_connection, so deriving the per-transport raw-accept ceiling from max_connections does not admit more real peers across multiple transports. Add effective_max_inbound on the TCP transport with a node_max_connections setter wired from create_transports, plus a precedence unit test. --- src/node/mod.rs | 8 ++++- src/transport/tcp/mod.rs | 65 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/node/mod.rs b/src/node/mod.rs index 8b183ee..3e96354 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -832,9 +832,15 @@ impl Node { .map(|(name, config)| (name.map(|s| s.to_string()), config.clone())) .collect(); + // Node-wide connection budget — used as the TCP inbound-cap fallback + // when a TCP instance has no explicit `max_inbound_connections`, so + // raising `node.limits.max_connections` actually raises the inbound + // ceiling rather than being silently capped at the transport default. + let node_max_connections = self.config.node.limits.max_connections; for (name, tcp_config) in tcp_instances { let transport_id = self.allocate_transport_id(); - let tcp = TcpTransport::new(transport_id, name, tcp_config, packet_tx.clone()); + let mut tcp = TcpTransport::new(transport_id, name, tcp_config, packet_tx.clone()); + tcp.set_node_max_connections(node_max_connections); transports.push(TransportHandle::Tcp(tcp)); } diff --git a/src/transport/tcp/mod.rs b/src/transport/tcp/mod.rs index f3c20fe..350c879 100644 --- a/src/transport/tcp/mod.rs +++ b/src/transport/tcp/mod.rs @@ -122,6 +122,10 @@ pub struct TcpTransport { accept_task: Option>, /// Local listener address (after start, if bind_addr configured). local_addr: Option, + /// Node-wide `node.limits.max_connections`, used as the inbound cap + /// fallback when this transport has no explicit `max_inbound_connections`. + /// `None` means "not provided" — fall through to the built-in default. + node_max_connections: Option, /// Transport statistics. stats: Arc, } @@ -144,10 +148,45 @@ impl TcpTransport { packet_tx, accept_task: None, local_addr: None, + node_max_connections: None, stats: Arc::new(TcpStats::new()), } } + /// Set the node-wide `node.limits.max_connections` value. + /// + /// Used as the inbound-cap fallback when this transport instance has no + /// explicit `transports.tcp.*.max_inbound_connections` set, so raising + /// `node.limits.max_connections` actually raises the per-transport TCP + /// accept ceiling instead of silently capping at the built-in default. + pub fn set_node_max_connections(&mut self, max: usize) { + self.node_max_connections = Some(max); + } + + /// Resolve the effective inbound connection cap for the accept loop. + /// + /// Precedence: explicit per-transport `max_inbound_connections` > + /// node-wide `node.limits.max_connections` > built-in default. This is a + /// per-transport *raw-accept* ceiling; the true node-wide peer budget is + /// still enforced downstream by the handshake-phase `max_connections` + /// admission check (`Node::add_connection`), so deriving this ceiling + /// from `max_connections` does not let multiple transports exceed the + /// node-wide total — it only stops the transport from rejecting inbound + /// below the configured node budget. + fn effective_max_inbound(&self) -> usize { + match ( + self.config.max_inbound_connections, + self.node_max_connections, + ) { + // Explicit per-transport key always wins. + (Some(explicit), _) => explicit, + // No per-transport key: fall back to the node-wide budget. + (None, Some(node_max)) => node_max, + // Neither set: the transport's built-in default (256). + (None, None) => self.config.max_inbound_connections(), + } + } + /// Get the instance name (if configured as a named instance). pub fn name(&self) -> Option<&str> { self.name.as_deref() @@ -197,7 +236,7 @@ impl TcpTransport { let stats = self.stats.clone(); let cfg = AcceptConfig { mtu: self.config.mtu(), - max_inbound: self.config.max_inbound_connections(), + max_inbound: self.effective_max_inbound(), nodelay: self.config.nodelay(), keepalive_secs: self.config.keepalive_secs(), recv_buf: self.config.recv_buf_size(), @@ -1156,6 +1195,30 @@ mod tests { transport.stop_async().await.unwrap(); } + #[test] + fn effective_max_inbound_precedence() { + let (tx, _rx) = packet_channel(100); + + // Neither set: built-in transport default (256). + let t = TcpTransport::new(TransportId::new(1), None, make_config(), tx.clone()); + assert_eq!(t.effective_max_inbound(), 256); + + // Node-wide max_connections drives the cap when no per-transport key. + let mut t = TcpTransport::new(TransportId::new(1), None, make_config(), tx.clone()); + t.set_node_max_connections(512); + assert_eq!(t.effective_max_inbound(), 512); + + // Explicit per-transport key wins over the node-wide value. + let cfg = TcpConfig { + bind_addr: Some("127.0.0.1:0".to_string()), + max_inbound_connections: Some(64), + ..Default::default() + }; + let mut t = TcpTransport::new(TransportId::new(1), None, cfg, tx); + t.set_node_max_connections(512); + assert_eq!(t.effective_max_inbound(), 64); + } + #[tokio::test] async fn test_double_start_fails() { let (tx, _rx) = packet_channel(100);