Skip to main content

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}