dynamo_runtime/pipeline/network/manager.rs
1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Network Manager - Single Source of Truth for Network Configuration
5//!
6//! This module consolidates ALL network-related configuration and creation logic.
7//! It is the ONLY place in the codebase that:
8//! - Reads environment variables for network configuration
9//! - Knows about transport-specific types
10//! - Performs mode selection based on RequestPlaneMode
11//! - Creates servers and clients
12//!
13//! The rest of the codebase works exclusively with trait objects and never
14//! directly accesses transport implementations or configuration.
15
16use super::egress::unified_client::RequestPlaneClient;
17use super::ingress::shared_tcp_endpoint::SharedTcpServer;
18use super::ingress::unified_server::RequestPlaneServer;
19use crate::config::environment_names::request_plane;
20use crate::distributed::RequestPlaneMode;
21use crate::utils::ip_resolver::{DefaultIpResolver, resolve_host_or_interface, resolve_local_host};
22use anyhow::Result;
23use async_once_cell::OnceCell;
24use std::net::SocketAddr;
25use std::sync::Arc;
26use std::sync::OnceLock;
27use tokio_util::sync::CancellationToken;
28
29/// Global storage for the advertised TCP RPC address after binding.
30/// Uses OnceLock since the shared server binds once and never changes.
31static ACTUAL_TCP_RPC_ADDRESS: OnceLock<SocketAddr> = OnceLock::new();
32
33/// Global storage for the shared TCP server instance.
34///
35/// When multiple workers run in the same process, they must share a single TCP server
36/// to ensure all endpoints are registered on the same server. Without this, each worker
37/// would create its own server on a different port, but all would publish the same port
38/// (from ACTUAL_TCP_RPC_ADDRESS) to discovery, causing "No handler found" errors.
39///
40/// Uses `tokio::sync::OnceCell` to support async initialization (binding the TCP socket).
41static GLOBAL_TCP_SERVER: tokio::sync::OnceCell<Arc<SharedTcpServer>> =
42 tokio::sync::OnceCell::const_new();
43
44/// Process-wide cancellation token for the global TCP server.
45///
46/// This token is independent of any individual runtime's cancellation token so that
47/// component Drop impls (e.g. KvRouter::drop → cancel) don't kill the shared accept
48/// loop while the OnceCell still hands out the (now-dead) server to later runtimes.
49static GLOBAL_TCP_SERVER_TOKEN: std::sync::LazyLock<CancellationToken> =
50 std::sync::LazyLock::new(CancellationToken::new);
51
52/// Get the actual TCP RPC port that the server is listening on.
53pub fn get_actual_tcp_rpc_port() -> anyhow::Result<u16> {
54 Ok(get_actual_tcp_rpc_address()?.port())
55}
56
57/// Get the concrete TCP RPC address published to discovery.
58pub(crate) fn get_actual_tcp_rpc_address() -> anyhow::Result<SocketAddr> {
59 ACTUAL_TCP_RPC_ADDRESS.get().copied().ok_or_else(|| {
60 tracing::error!(
61 "TCP RPC address not set - request_plane_server() must be called before get_actual_tcp_rpc_address()"
62 );
63 anyhow::anyhow!("TCP RPC address not initialized. This is not expected.")
64 })
65}
66
67/// Set the address published to discovery after the shared server binds.
68fn set_actual_tcp_rpc_address(address: SocketAddr) {
69 if let Err(existing) = ACTUAL_TCP_RPC_ADDRESS.set(address) {
70 tracing::warn!(
71 %existing,
72 new_address = %address,
73 "TCP RPC address already set, ignoring new value"
74 );
75 }
76}
77
78/// Network configuration loaded from environment variables
79#[derive(Clone)]
80struct NetworkConfig {
81 // TCP server configuration
82 tcp_host: Option<String>,
83 /// TCP port to bind to. If None, the OS will assign a free port.
84 tcp_port: Option<u16>,
85
86 // TCP client configuration
87 tcp_client_config: super::egress::tcp_client::TcpRequestConfig,
88
89 // NATS configuration (provided externally, not from env)
90 nats_client: Option<async_nats::Client>,
91}
92
93impl NetworkConfig {
94 /// Load configuration from environment variables
95 ///
96 /// This is the ONLY place where network-related environment variables are read.
97 fn from_env(nats_client: Option<async_nats::Client>) -> Self {
98 Self {
99 // TCP server configuration
100 // If DYN_TCP_RPC_PORT is set, use that port; otherwise None means OS will assign a free port
101 tcp_host: std::env::var(request_plane::DYN_TCP_RPC_HOST)
102 .ok()
103 .filter(|host| !host.is_empty()),
104 tcp_port: std::env::var(request_plane::DYN_TCP_RPC_PORT)
105 .ok()
106 .and_then(|p| p.parse().ok()),
107
108 // TCP client configuration (reads DYN_TCP_* env vars)
109 tcp_client_config: super::egress::tcp_client::TcpRequestConfig::from_env(),
110
111 // NATS (external)
112 nats_client,
113 }
114 }
115}
116
117/// Network Manager - Central coordinator for all network resources
118///
119/// # Responsibilities
120///
121/// 1. **Configuration Management**: Reads and manages all network-related environment variables
122/// 2. **Server Creation**: Creates and starts request plane servers based on mode
123/// 3. **Client Creation**: Creates request plane clients on demand
124/// 4. **Abstraction**: Hides all transport-specific details from the rest of the codebase
125///
126/// # Design Principles
127///
128/// - **Single Source of Truth**: All network config and creation logic lives here
129/// - **Lazy Initialization**: Servers are created only when first accessed
130/// - **Transport Agnostic Interface**: Exposes only trait objects to callers
131/// - **No Leaky Abstractions**: Transport types never escape this module
132///
133/// # Example
134///
135/// ```ignore
136/// // Create manager (typically done once in DistributedRuntime)
137/// let manager = NetworkManager::new(cancel_token, nats_client, component_registry, request_plane_mode);
138///
139/// // Get server (lazy init, cached)
140/// let server = manager.server().await?;
141/// server.register_endpoint(...).await?;
142///
143/// // Create client (not cached, lightweight)
144/// let client = manager.create_client()?;
145/// client.send_request(...).await?;
146/// ```
147pub struct NetworkManager {
148 mode: RequestPlaneMode,
149 config: NetworkConfig,
150 server: Arc<OnceCell<Arc<dyn RequestPlaneServer>>>,
151 cancellation_token: CancellationToken,
152 component_registry: crate::component::Registry,
153}
154
155impl NetworkManager {
156 /// Create a new network manager
157 ///
158 /// This is the single constructor for NetworkManager. All configuration
159 /// is loaded from environment variables internally.
160 ///
161 /// # Arguments
162 ///
163 /// * `cancellation_token` - Token for graceful shutdown of servers
164 /// * `nats_client` - Optional NATS client (required only for NATS mode)
165 /// * `component_registry` - Component registry to get NATS service groups from
166 ///
167 /// # Returns
168 ///
169 /// Returns an Arc-wrapped NetworkManager ready to create servers and clients.
170 pub fn new(
171 cancellation_token: CancellationToken,
172 nats_client: Option<async_nats::Client>,
173 component_registry: crate::component::Registry,
174 mode: RequestPlaneMode,
175 ) -> Self {
176 let config = NetworkConfig::from_env(nats_client);
177
178 match mode {
179 RequestPlaneMode::Tcp => {
180 let port_display = config
181 .tcp_port
182 .map(|p| p.to_string())
183 .unwrap_or_else(|| "OS-assigned".to_string());
184 tracing::info!(
185 %mode,
186 host = config.tcp_host.as_deref().unwrap_or("auto"),
187 port = %port_display,
188 "Initializing NetworkManager with TCP request plane"
189 );
190 }
191 RequestPlaneMode::Nats => {
192 tracing::info!(
193 %mode,
194 "Initializing NetworkManager with NATS request plane"
195 );
196 }
197 }
198
199 Self {
200 mode,
201 config,
202 server: Arc::new(OnceCell::new()),
203 cancellation_token,
204 component_registry,
205 }
206 }
207
208 /// Get or create the request plane server
209 ///
210 /// The server is created lazily on first access and cached for subsequent calls.
211 /// The server is automatically started in the background.
212 ///
213 /// # Returns
214 ///
215 /// Returns a trait object that abstracts over TCP/NATS implementations.
216 ///
217 /// # Errors
218 ///
219 /// Returns an error if:
220 /// - Server creation fails (e.g., port already in use)
221 /// - NATS mode is selected but NATS client is not available
222 /// - Configuration is invalid (e.g., malformed bind address)
223 pub async fn server(&self) -> Result<Arc<dyn RequestPlaneServer>> {
224 let server = self
225 .server
226 .get_or_try_init(async { self.create_server().await })
227 .await?;
228
229 Ok(server.clone())
230 }
231
232 /// Create a new request plane client
233 ///
234 /// Clients are lightweight and not cached. Each call creates a new client instance.
235 ///
236 /// # Returns
237 ///
238 /// Returns a trait object that abstracts over TCP/NATS implementations.
239 ///
240 /// # Errors
241 ///
242 /// Returns an error if:
243 /// - Client creation fails (e.g., invalid configuration)
244 /// - NATS mode is selected but NATS client is not available
245 pub fn create_client(&self) -> Result<Arc<dyn RequestPlaneClient>> {
246 match self.mode {
247 RequestPlaneMode::Tcp => self.create_tcp_client(),
248 RequestPlaneMode::Nats => self.create_nats_client(),
249 }
250 }
251
252 /// Get the current request plane mode
253 ///
254 /// This is provided primarily for logging and debugging purposes.
255 /// Application logic should not branch on mode - use trait objects instead.
256 pub fn mode(&self) -> RequestPlaneMode {
257 self.mode
258 }
259
260 // ============================================================================
261 // PRIVATE: Server Creation
262 // ============================================================================
263
264 async fn create_server(&self) -> Result<Arc<dyn RequestPlaneServer>> {
265 match self.mode {
266 RequestPlaneMode::Tcp => self.create_tcp_server().await,
267 RequestPlaneMode::Nats => self.create_nats_server().await,
268 }
269 }
270
271 async fn create_tcp_server(&self) -> Result<Arc<dyn RequestPlaneServer>> {
272 // Use the global TCP server to ensure all workers in the same process share
273 // a single server. This is critical for correct endpoint routing.
274 let server = GLOBAL_TCP_SERVER
275 .get_or_try_init(|| async {
276 // Use configured port if specified, otherwise use port 0 (OS assigns free port)
277 let port = self.config.tcp_port.unwrap_or(0);
278 let resolver = DefaultIpResolver;
279 let resolved_host = match self.config.tcp_host.as_deref() {
280 Some(host) => resolve_host_or_interface(host, &resolver).map_err(|error| {
281 anyhow::anyhow!("Failed to resolve configured TCP RPC host '{host}': {error}")
282 })?,
283 None => resolve_local_host(&resolver).map_err(|error| {
284 anyhow::anyhow!("Failed to resolve local TCP RPC host: {error}")
285 })?,
286 };
287 let bind_addr = SocketAddr::new(resolved_host.bind_ip(), port);
288
289 tracing::info!(
290 bind_addr = %bind_addr,
291 port_source = if self.config.tcp_port.is_some() { request_plane::DYN_TCP_RPC_PORT } else { "OS-assigned" },
292 "Creating TCP request plane server"
293 );
294
295 let server = SharedTcpServer::new(bind_addr, GLOBAL_TCP_SERVER_TOKEN.clone())?;
296
297 // Bind and start server, getting the actual bound address
298 let actual_addr = server.clone().bind_and_start().await?;
299
300 let advertised_addr =
301 SocketAddr::new(resolved_host.advertise_ip(), actual_addr.port());
302 set_actual_tcp_rpc_address(advertised_addr);
303
304 tracing::info!(
305 actual_addr = %actual_addr,
306 %advertised_addr,
307 "TCP request plane server started"
308 );
309
310 Ok::<_, anyhow::Error>(server)
311 })
312 .await?;
313
314 Ok(server.clone() as Arc<dyn RequestPlaneServer>)
315 }
316
317 async fn create_nats_server(&self) -> Result<Arc<dyn RequestPlaneServer>> {
318 use super::ingress::nats_server::NatsMultiplexedServer;
319
320 let nats_client = self
321 .config
322 .nats_client
323 .as_ref()
324 .ok_or_else(|| anyhow::anyhow!("NATS client required for NATS mode"))?;
325
326 tracing::info!("Creating NATS request plane server");
327
328 Ok(NatsMultiplexedServer::new(
329 nats_client.clone(),
330 self.component_registry.clone(),
331 self.cancellation_token.clone(),
332 ) as Arc<dyn RequestPlaneServer>)
333 }
334
335 // ============================================================================
336 // PRIVATE: Client Creation
337 // ============================================================================
338
339 fn create_tcp_client(&self) -> Result<Arc<dyn RequestPlaneClient>> {
340 use super::egress::tcp_client::TcpRequestClient;
341
342 tracing::debug!("Creating TCP request plane client with config from NetworkManager");
343 Ok(Arc::new(TcpRequestClient::with_config(
344 self.config.tcp_client_config.clone(),
345 )?))
346 }
347
348 fn create_nats_client(&self) -> Result<Arc<dyn RequestPlaneClient>> {
349 use super::egress::nats_client::NatsRequestClient;
350
351 let nats_client = self
352 .config
353 .nats_client
354 .as_ref()
355 .ok_or_else(|| anyhow::anyhow!("NATS client required for NATS mode"))?;
356
357 tracing::debug!("Creating NATS request plane client");
358 Ok(Arc::new(NatsRequestClient::new(nats_client.clone())))
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 fn manager_for(mode: RequestPlaneMode) -> NetworkManager {
367 NetworkManager::new(
368 CancellationToken::new(),
369 None,
370 crate::component::Registry::new(),
371 mode,
372 )
373 }
374
375 #[test]
376 fn tcp_mode_creates_tcp_client_without_nats_client() {
377 let tcp = manager_for(RequestPlaneMode::Tcp).create_client().unwrap();
378 assert_eq!(tcp.transport_name(), "tcp");
379 }
380
381 #[test]
382 fn nats_mode_requires_nats_client() {
383 match manager_for(RequestPlaneMode::Nats).create_client() {
384 Ok(client) => panic!(
385 "expected NATS mode without NATS client to fail, got {} client",
386 client.transport_name()
387 ),
388 Err(err) => assert!(err.to_string().contains("NATS client required")),
389 }
390 }
391}