dynamo_runtime/
config.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use anyhow::Result;
5use derive_builder::Builder;
6use figment::{
7    Figment,
8    providers::{Env, Format, Serialized, Toml},
9};
10use serde::{Deserialize, Serialize};
11use std::fmt;
12use std::sync::OnceLock;
13use validator::Validate;
14
15/// Default system host for health and metrics endpoints
16const DEFAULT_SYSTEM_HOST: &str = "0.0.0.0";
17
18/// Default system port for health and metrics endpoints (-1 = disabled)
19const DEFAULT_SYSTEM_PORT: i16 = -1;
20
21/// Default health endpoint paths
22const DEFAULT_SYSTEM_HEALTH_PATH: &str = "/health";
23const DEFAULT_SYSTEM_LIVE_PATH: &str = "/live";
24
25/// Default health check configuration
26/// This is the wait time before sending canary health checks when no activity is detected
27pub const DEFAULT_CANARY_WAIT_TIME_SECS: u64 = 10;
28/// Default timeout for individual health check requests
29pub const DEFAULT_HEALTH_CHECK_REQUEST_TIMEOUT_SECS: u64 = 3;
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct WorkerConfig {
33    /// Grace shutdown period for the system server.
34    pub graceful_shutdown_timeout: u64,
35}
36
37impl WorkerConfig {
38    /// Instantiates and reads server configurations from appropriate sources.
39    /// Panics on invalid configuration.
40    pub fn from_settings() -> Self {
41        // All calls should be global and thread safe.
42        Figment::new()
43            .merge(Serialized::defaults(Self::default()))
44            .merge(Env::prefixed("DYN_WORKER_"))
45            .extract()
46            .unwrap() // safety: Called on startup, so panic is reasonable
47    }
48}
49
50impl Default for WorkerConfig {
51    fn default() -> Self {
52        WorkerConfig {
53            graceful_shutdown_timeout: if cfg!(debug_assertions) {
54                1 // Debug build: 1 second
55            } else {
56                30 // Release build: 30 seconds
57            },
58        }
59    }
60}
61
62#[derive(Debug, Deserialize, Serialize, PartialEq, Clone)]
63#[serde(rename_all = "lowercase")]
64pub enum HealthStatus {
65    Ready,
66    NotReady,
67}
68
69/// Runtime configuration
70/// Defines the configuration for Tokio runtimes
71#[derive(Serialize, Deserialize, Validate, Debug, Builder, Clone)]
72#[builder(build_fn(private, name = "build_internal"), derive(Debug, Serialize))]
73pub struct RuntimeConfig {
74    /// Number of async worker threads
75    /// If set to 1, the runtime will run in single-threaded mode
76    /// Set this at runtime with environment variable DYN_RUNTIME_NUM_WORKER_THREADS. Defaults to
77    /// number of cores.
78    #[validate(range(min = 1))]
79    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
80    pub num_worker_threads: Option<usize>,
81
82    /// Maximum number of blocking threads
83    /// Blocking threads are used for blocking operations, this value must be greater than 0.
84    /// Set this at runtime with environment variable DYN_RUNTIME_MAX_BLOCKING_THREADS. Defaults to
85    /// 512.
86    #[validate(range(min = 1))]
87    #[builder(default = "512")]
88    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
89    pub max_blocking_threads: usize,
90
91    /// System status server host for health and metrics endpoints
92    /// Set this at runtime with environment variable DYN_SYSTEM_HOST
93    #[builder(default = "DEFAULT_SYSTEM_HOST.to_string()")]
94    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
95    pub system_host: String,
96
97    /// System status server port for health and metrics endpoints
98    /// Set to -1 to disable the system status server (default)
99    /// Set to a positive port number (e.g. 8081) to enable it
100    /// Set this at runtime with environment variable DYN_SYSTEM_PORT
101    #[builder(default = "DEFAULT_SYSTEM_PORT")]
102    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
103    pub system_port: i16,
104
105    /// Health and metrics System status server enabled (DEPRECATED)
106    /// This field is deprecated. Use system_port instead (set to positive value to enable)
107    /// Environment variable DYN_SYSTEM_ENABLED is deprecated
108    #[deprecated(
109        note = "Use system_port instead. Set DYN_SYSTEM_PORT to enable the system metrics server."
110    )]
111    #[builder(default = "false")]
112    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
113    pub system_enabled: bool,
114
115    /// Starting Health Status
116    /// Set this at runtime with environment variable DYN_SYSTEM_STARTING_HEALTH_STATUS
117    #[builder(default = "HealthStatus::NotReady")]
118    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
119    pub starting_health_status: HealthStatus,
120
121    /// Use Endpoint Health Status
122    /// When using endpoint health status, health status
123    /// is the AND of individual endpoint health
124    /// Set this at runtime with environment variable DYN_SYSTEM_USE_ENDPOINT_HEALTH_STATUS
125    /// with the list of endpoints to consider for system health
126    #[builder(default = "vec![]")]
127    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
128    pub use_endpoint_health_status: Vec<String>,
129
130    /// Health endpoint paths
131    /// Set this at runtime with environment variable DYN_SYSTEM_HEALTH_PATH
132    #[builder(default = "DEFAULT_SYSTEM_HEALTH_PATH.to_string()")]
133    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
134    pub system_health_path: String,
135    /// Set this at runtime with environment variable DYN_SYSTEM_LIVE_PATH
136    #[builder(default = "DEFAULT_SYSTEM_LIVE_PATH.to_string()")]
137    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
138    pub system_live_path: String,
139
140    /// Number of threads for the Rayon compute pool
141    /// If not set, defaults to num_cpus / 2
142    /// Set this at runtime with environment variable DYN_COMPUTE_THREADS
143    #[builder(default = "None")]
144    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
145    pub compute_threads: Option<usize>,
146
147    /// Stack size for compute threads in bytes
148    /// Defaults to 2MB (2097152 bytes)
149    /// Set this at runtime with environment variable DYN_COMPUTE_STACK_SIZE
150    #[builder(default = "Some(2 * 1024 * 1024)")]
151    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
152    pub compute_stack_size: Option<usize>,
153
154    /// Thread name prefix for compute pool threads
155    /// Set this at runtime with environment variable DYN_COMPUTE_THREAD_PREFIX
156    #[builder(default = "\"compute\".to_string()")]
157    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
158    pub compute_thread_prefix: String,
159
160    /// Enable active health checking with payloads
161    /// Set this at runtime with environment variable DYN_HEALTH_CHECK_ENABLED
162    #[builder(default = "false")]
163    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
164    pub health_check_enabled: bool,
165
166    /// Canary wait time in seconds (time to wait before sending health check when no activity)
167    /// Set this at runtime with environment variable DYN_CANARY_WAIT_TIME
168    #[builder(default = "DEFAULT_CANARY_WAIT_TIME_SECS")]
169    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
170    pub canary_wait_time_secs: u64,
171
172    /// Health check request timeout in seconds
173    /// Set this at runtime with environment variable DYN_HEALTH_CHECK_REQUEST_TIMEOUT
174    #[builder(default = "DEFAULT_HEALTH_CHECK_REQUEST_TIMEOUT_SECS")]
175    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
176    pub health_check_request_timeout_secs: u64,
177}
178
179impl fmt::Display for RuntimeConfig {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        // If None, it defaults to "number of cores", so we indicate that.
182        match self.num_worker_threads {
183            Some(val) => write!(f, "num_worker_threads={val}, ")?,
184            None => write!(f, "num_worker_threads=default (num_cores), ")?,
185        }
186
187        write!(f, "max_blocking_threads={}, ", self.max_blocking_threads)?;
188        write!(f, "system_host={}, ", self.system_host)?;
189        write!(f, "system_port={}, ", self.system_port)?;
190        write!(
191            f,
192            "use_endpoint_health_status={:?}",
193            self.use_endpoint_health_status
194        )?;
195        write!(
196            f,
197            "starting_health_status={:?}",
198            self.starting_health_status
199        )?;
200        write!(f, ", system_health_path={}", self.system_health_path)?;
201        write!(f, ", system_live_path={}", self.system_live_path)?;
202        write!(f, ", health_check_enabled={}", self.health_check_enabled)?;
203        write!(f, ", canary_wait_time_secs={}", self.canary_wait_time_secs)?;
204        write!(
205            f,
206            ", health_check_request_timeout_secs={}",
207            self.health_check_request_timeout_secs
208        )?;
209
210        Ok(())
211    }
212}
213
214impl RuntimeConfig {
215    pub fn builder() -> RuntimeConfigBuilder {
216        RuntimeConfigBuilder::default()
217    }
218
219    pub(crate) fn figment() -> Figment {
220        Figment::new()
221            .merge(Serialized::defaults(RuntimeConfig::default()))
222            .merge(Toml::file("/opt/dynamo/defaults/runtime.toml"))
223            .merge(Toml::file("/opt/dynamo/etc/runtime.toml"))
224            .merge(Env::prefixed("DYN_RUNTIME_").filter_map(|k| {
225                let full_key = format!("DYN_RUNTIME_{}", k.as_str());
226                // filters out empty environment variables
227                match std::env::var(&full_key) {
228                    Ok(v) if !v.is_empty() => Some(k.into()),
229                    _ => None,
230                }
231            }))
232            .merge(Env::prefixed("DYN_SYSTEM_").filter_map(|k| {
233                let full_key = format!("DYN_SYSTEM_{}", k.as_str());
234                // filters out empty environment variables
235                match std::env::var(&full_key) {
236                    Ok(v) if !v.is_empty() => {
237                        // Map DYN_SYSTEM_* to the correct field names
238                        let mapped_key = match k.as_str() {
239                            "HOST" => "system_host",
240                            "PORT" => "system_port",
241                            "ENABLED" => "system_enabled",
242                            "USE_ENDPOINT_HEALTH_STATUS" => "use_endpoint_health_status",
243                            "STARTING_HEALTH_STATUS" => "starting_health_status",
244                            "HEALTH_PATH" => "system_health_path",
245                            "LIVE_PATH" => "system_live_path",
246                            _ => k.as_str(),
247                        };
248                        Some(mapped_key.into())
249                    }
250                    _ => None,
251                }
252            }))
253            .merge(Env::prefixed("DYN_COMPUTE_").filter_map(|k| {
254                let full_key = format!("DYN_COMPUTE_{}", k.as_str());
255                // filters out empty environment variables
256                match std::env::var(&full_key) {
257                    Ok(v) if !v.is_empty() => {
258                        // Map DYN_COMPUTE_* to the correct field names
259                        let mapped_key = match k.as_str() {
260                            "THREADS" => "compute_threads",
261                            "STACK_SIZE" => "compute_stack_size",
262                            "THREAD_PREFIX" => "compute_thread_prefix",
263                            _ => k.as_str(),
264                        };
265                        Some(mapped_key.into())
266                    }
267                    _ => None,
268                }
269            }))
270            .merge(Env::prefixed("DYN_HEALTH_CHECK_").filter_map(|k| {
271                let full_key = format!("DYN_HEALTH_CHECK_{}", k.as_str());
272                // filters out empty environment variables
273                match std::env::var(&full_key) {
274                    Ok(v) if !v.is_empty() => {
275                        // Map DYN_HEALTH_CHECK_* to the correct field names
276                        let mapped_key = match k.as_str() {
277                            "ENABLED" => "health_check_enabled",
278                            "REQUEST_TIMEOUT" => "health_check_request_timeout_secs",
279                            _ => k.as_str(),
280                        };
281                        Some(mapped_key.into())
282                    }
283                    _ => None,
284                }
285            }))
286            .merge(Env::prefixed("DYN_CANARY_").filter_map(|k| {
287                let full_key = format!("DYN_CANARY_{}", k.as_str());
288                // filters out empty environment variables
289                match std::env::var(&full_key) {
290                    Ok(v) if !v.is_empty() => {
291                        // Map DYN_CANARY_* to the correct field names
292                        let mapped_key = match k.as_str() {
293                            "WAIT_TIME" => "canary_wait_time_secs",
294                            _ => k.as_str(),
295                        };
296                        Some(mapped_key.into())
297                    }
298                    _ => None,
299                }
300            }))
301    }
302
303    /// Load the runtime configuration from the environment and configuration files
304    /// Configuration is priorities in the following order, where the last has the lowest priority:
305    /// 1. Environment variables (top priority)
306    ///    TO DO: Add documentation for configuration files. Paths should be configurable.
307    /// 2. /opt/dynamo/etc/runtime.toml
308    /// 3. /opt/dynamo/defaults/runtime.toml (lowest priority)
309    ///
310    /// Environment variables are prefixed with `DYN_RUNTIME_` and `DYN_SYSTEM`
311    pub fn from_settings() -> Result<RuntimeConfig> {
312        // Check for deprecated environment variables
313        if std::env::var("DYN_SYSTEM_USE_ENDPOINT_HEALTH_STATUS").is_ok() {
314            tracing::warn!(
315                "DYN_SYSTEM_USE_ENDPOINT_HEALTH_STATUS is deprecated and no longer used. \
316                System health is now determined by endpoints that register with health check payloads. \
317                Please update your configuration to register health check payloads directly on endpoints."
318            );
319        }
320
321        if std::env::var("DYN_SYSTEM_ENABLED").is_ok() {
322            tracing::warn!(
323                "DYN_SYSTEM_ENABLED is deprecated. \
324                System metrics server is now controlled solely by DYN_SYSTEM_PORT. \
325                Set DYN_SYSTEM_PORT to a positive value to enable the server, or set to -1 to disable (default)."
326            );
327        }
328
329        let config: RuntimeConfig = Self::figment().extract()?;
330        config.validate()?;
331        Ok(config)
332    }
333
334    /// Check if System server should be enabled
335    /// System server is enabled when DYN_SYSTEM_PORT is set to a positive value
336    /// Negative values disable the server
337    /// TODO: Support port = 0 to bind to a random available port
338    pub fn system_server_enabled(&self) -> bool {
339        self.system_port > 0
340    }
341
342    pub fn single_threaded() -> Self {
343        RuntimeConfig {
344            num_worker_threads: Some(1),
345            max_blocking_threads: 1,
346            system_host: DEFAULT_SYSTEM_HOST.to_string(),
347            system_port: DEFAULT_SYSTEM_PORT,
348            #[allow(deprecated)]
349            system_enabled: false,
350            starting_health_status: HealthStatus::NotReady,
351            use_endpoint_health_status: vec![],
352            system_health_path: DEFAULT_SYSTEM_HEALTH_PATH.to_string(),
353            system_live_path: DEFAULT_SYSTEM_LIVE_PATH.to_string(),
354            compute_threads: Some(1),
355            compute_stack_size: Some(2 * 1024 * 1024),
356            compute_thread_prefix: "compute".to_string(),
357            health_check_enabled: false,
358            canary_wait_time_secs: DEFAULT_CANARY_WAIT_TIME_SECS,
359            health_check_request_timeout_secs: DEFAULT_HEALTH_CHECK_REQUEST_TIMEOUT_SECS,
360        }
361    }
362
363    /// Create a new default runtime configuration
364    pub(crate) fn create_runtime(&self) -> std::io::Result<tokio::runtime::Runtime> {
365        tokio::runtime::Builder::new_multi_thread()
366            .worker_threads(
367                self.num_worker_threads
368                    .unwrap_or_else(|| std::thread::available_parallelism().unwrap().get()),
369            )
370            .max_blocking_threads(self.max_blocking_threads)
371            .enable_all()
372            .build()
373    }
374}
375
376impl Default for RuntimeConfig {
377    fn default() -> Self {
378        let num_cores = std::thread::available_parallelism().unwrap().get();
379        Self {
380            num_worker_threads: Some(num_cores),
381            max_blocking_threads: num_cores,
382            system_host: DEFAULT_SYSTEM_HOST.to_string(),
383            system_port: DEFAULT_SYSTEM_PORT,
384            #[allow(deprecated)]
385            system_enabled: false,
386            starting_health_status: HealthStatus::NotReady,
387            use_endpoint_health_status: vec![],
388            system_health_path: DEFAULT_SYSTEM_HEALTH_PATH.to_string(),
389            system_live_path: DEFAULT_SYSTEM_LIVE_PATH.to_string(),
390            compute_threads: None,
391            compute_stack_size: Some(2 * 1024 * 1024),
392            compute_thread_prefix: "compute".to_string(),
393            health_check_enabled: false,
394            canary_wait_time_secs: DEFAULT_CANARY_WAIT_TIME_SECS,
395            health_check_request_timeout_secs: DEFAULT_HEALTH_CHECK_REQUEST_TIMEOUT_SECS,
396        }
397    }
398}
399
400impl RuntimeConfigBuilder {
401    /// Build and validate the runtime configuration
402    pub fn build(&self) -> Result<RuntimeConfig> {
403        let config = self.build_internal()?;
404        config.validate()?;
405        Ok(config)
406    }
407}
408
409/// Check if a string is truthy
410/// This will be used to evaluate environment variables or any other subjective
411/// configuration parameters that can be set by the user that should be evaluated
412/// as a boolean value.
413pub fn is_truthy(val: &str) -> bool {
414    matches!(val.to_lowercase().as_str(), "1" | "true" | "on" | "yes")
415}
416
417pub fn parse_bool(val: &str) -> anyhow::Result<bool> {
418    if is_truthy(val) {
419        Ok(true)
420    } else if is_falsey(val) {
421        Ok(false)
422    } else {
423        anyhow::bail!(
424            "Invalid boolean value: '{}'. Expected one of: true/false, 1/0, on/off, yes/no",
425            val
426        )
427    }
428}
429
430/// Check if a string is falsey
431/// This will be used to evaluate environment variables or any other subjective
432/// configuration parameters that can be set by the user that should be evaluated
433/// as a boolean value (opposite of is_truthy).
434pub fn is_falsey(val: &str) -> bool {
435    matches!(val.to_lowercase().as_str(), "0" | "false" | "off" | "no")
436}
437
438/// Check if an environment variable is truthy
439pub fn env_is_truthy(env: &str) -> bool {
440    match std::env::var(env) {
441        Ok(val) => is_truthy(val.as_str()),
442        Err(_) => false,
443    }
444}
445
446/// Check if an environment variable is falsey
447pub fn env_is_falsey(env: &str) -> bool {
448    match std::env::var(env) {
449        Ok(val) => is_falsey(val.as_str()),
450        Err(_) => false,
451    }
452}
453
454/// Check whether JSONL logging enabled
455/// Set the `DYN_LOGGING_JSONL` environment variable a [`is_truthy`] value
456pub fn jsonl_logging_enabled() -> bool {
457    env_is_truthy("DYN_LOGGING_JSONL")
458}
459
460/// Check whether logging with ANSI terminal escape codes and colors is disabled.
461/// Set the `DYN_SDK_DISABLE_ANSI_LOGGING` environment variable a [`is_truthy`] value
462pub fn disable_ansi_logging() -> bool {
463    env_is_truthy("DYN_SDK_DISABLE_ANSI_LOGGING")
464}
465
466/// Check whether to use local timezone for logging timestamps (default is UTC)
467/// Set the `DYN_LOG_USE_LOCAL_TZ` environment variable to a [`is_truthy`] value
468pub fn use_local_timezone() -> bool {
469    env_is_truthy("DYN_LOG_USE_LOCAL_TZ")
470}
471
472/// Request plane transport mode configuration
473///
474/// This determines how requests are distributed from routers to workers:
475/// - `Nats`: Use NATS for request distribution (default, legacy)
476/// - `Http`: Use HTTP/2 for request distribution
477/// - `Tcp`: Use raw TCP for request distribution with msgpack support
478#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
479#[serde(rename_all = "lowercase")]
480pub enum RequestPlaneMode {
481    /// Use NATS for request plane (default for backward compatibility)
482    Nats,
483    /// Use HTTP/2 for request plane
484    Http,
485    /// Use raw TCP for request plane with msgpack support
486    Tcp,
487}
488
489impl Default for RequestPlaneMode {
490    fn default() -> Self {
491        Self::Nats
492    }
493}
494
495impl fmt::Display for RequestPlaneMode {
496    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
497        match self {
498            Self::Nats => write!(f, "nats"),
499            Self::Http => write!(f, "http"),
500            Self::Tcp => write!(f, "tcp"),
501        }
502    }
503}
504
505impl std::str::FromStr for RequestPlaneMode {
506    type Err = anyhow::Error;
507
508    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
509        match s.to_lowercase().as_str() {
510            "nats" => Ok(Self::Nats),
511            "http" => Ok(Self::Http),
512            "tcp" => Ok(Self::Tcp),
513            _ => Err(anyhow::anyhow!(
514                "Invalid request plane mode: '{}'. Valid options are: 'nats', 'http', 'tcp'",
515                s
516            )),
517        }
518    }
519}
520
521/// Global cached request plane mode
522static REQUEST_PLANE_MODE: OnceLock<RequestPlaneMode> = OnceLock::new();
523
524impl RequestPlaneMode {
525    /// The cached request plane mode, initialized from `DYN_REQUEST_PLANE` environment variable
526    /// or defaulting to NATS if not set or invalid.
527    pub fn get() -> Self {
528        *REQUEST_PLANE_MODE.get_or_init(Self::from_env)
529    }
530
531    /// Get the request plane mode from environment variable (uncached)
532    /// Reads from `DYN_REQUEST_PLANE` environment variable.
533    pub fn from_env() -> Self {
534        std::env::var("DYN_REQUEST_PLANE")
535            .ok()
536            .and_then(|s| s.parse().ok())
537            .unwrap_or_default()
538    }
539}
540
541#[cfg(test)]
542mod tests {
543    use super::*;
544
545    #[test]
546    fn test_runtime_config_with_env_vars() -> Result<()> {
547        temp_env::with_vars(
548            vec![
549                ("DYN_RUNTIME_NUM_WORKER_THREADS", Some("24")),
550                ("DYN_RUNTIME_MAX_BLOCKING_THREADS", Some("32")),
551            ],
552            || {
553                let config = RuntimeConfig::from_settings()?;
554                assert_eq!(config.num_worker_threads, Some(24));
555                assert_eq!(config.max_blocking_threads, 32);
556                Ok(())
557            },
558        )
559    }
560
561    #[test]
562    fn test_runtime_config_defaults() -> Result<()> {
563        temp_env::with_vars(
564            vec![
565                ("DYN_RUNTIME_NUM_WORKER_THREADS", None::<&str>),
566                ("DYN_RUNTIME_MAX_BLOCKING_THREADS", Some("")),
567            ],
568            || {
569                let config = RuntimeConfig::from_settings()?;
570
571                let default_config = RuntimeConfig::default();
572                assert_eq!(config.num_worker_threads, default_config.num_worker_threads);
573                assert_eq!(
574                    config.max_blocking_threads,
575                    default_config.max_blocking_threads
576                );
577                Ok(())
578            },
579        )
580    }
581
582    #[test]
583    fn test_runtime_config_rejects_invalid_thread_count() -> Result<()> {
584        temp_env::with_vars(
585            vec![
586                ("DYN_RUNTIME_NUM_WORKER_THREADS", Some("0")),
587                ("DYN_RUNTIME_MAX_BLOCKING_THREADS", Some("0")),
588            ],
589            || {
590                let result = RuntimeConfig::from_settings();
591                assert!(result.is_err());
592                if let Err(e) = result {
593                    assert!(
594                        e.to_string()
595                            .contains("num_worker_threads: Validation error")
596                    );
597                    assert!(
598                        e.to_string()
599                            .contains("max_blocking_threads: Validation error")
600                    );
601                }
602                Ok(())
603            },
604        )
605    }
606
607    #[test]
608    fn test_runtime_config_system_server_env_vars() -> Result<()> {
609        temp_env::with_vars(
610            vec![
611                ("DYN_SYSTEM_HOST", Some("127.0.0.1")),
612                ("DYN_SYSTEM_PORT", Some("9090")),
613            ],
614            || {
615                let config = RuntimeConfig::from_settings()?;
616                assert_eq!(config.system_host, "127.0.0.1");
617                assert_eq!(config.system_port, 9090);
618                Ok(())
619            },
620        )
621    }
622
623    #[test]
624    fn test_system_server_disabled_by_default() {
625        temp_env::with_vars(vec![("DYN_SYSTEM_PORT", None::<&str>)], || {
626            let config = RuntimeConfig::from_settings().unwrap();
627            assert!(!config.system_server_enabled());
628            assert_eq!(config.system_port, -1);
629        });
630    }
631
632    #[test]
633    fn test_system_server_disabled_with_negative_port() {
634        temp_env::with_vars(vec![("DYN_SYSTEM_PORT", Some("-1"))], || {
635            let config = RuntimeConfig::from_settings().unwrap();
636            assert!(!config.system_server_enabled());
637            assert_eq!(config.system_port, -1);
638        });
639    }
640
641    #[test]
642    fn test_system_server_enabled_with_port() {
643        temp_env::with_vars(vec![("DYN_SYSTEM_PORT", Some("9527"))], || {
644            let config = RuntimeConfig::from_settings().unwrap();
645            assert!(config.system_server_enabled());
646            assert_eq!(config.system_port, 9527);
647        });
648    }
649
650    #[test]
651    fn test_system_server_starting_health_status_ready() {
652        temp_env::with_vars(
653            vec![("DYN_SYSTEM_STARTING_HEALTH_STATUS", Some("ready"))],
654            || {
655                let config = RuntimeConfig::from_settings().unwrap();
656                assert!(config.starting_health_status == HealthStatus::Ready);
657            },
658        );
659    }
660
661    #[test]
662    fn test_system_use_endpoint_health_status() {
663        temp_env::with_vars(
664            vec![("DYN_SYSTEM_USE_ENDPOINT_HEALTH_STATUS", Some("[\"ready\"]"))],
665            || {
666                let config = RuntimeConfig::from_settings().unwrap();
667                assert!(config.use_endpoint_health_status == vec!["ready"]);
668            },
669        );
670    }
671
672    #[test]
673    fn test_system_health_endpoint_path_default() {
674        temp_env::with_vars(vec![("DYN_SYSTEM_HEALTH_PATH", None::<&str>)], || {
675            let config = RuntimeConfig::from_settings().unwrap();
676            assert_eq!(
677                config.system_health_path,
678                DEFAULT_SYSTEM_HEALTH_PATH.to_string()
679            );
680        });
681
682        temp_env::with_vars(vec![("DYN_SYSTEM_LIVE_PATH", None::<&str>)], || {
683            let config = RuntimeConfig::from_settings().unwrap();
684            assert_eq!(
685                config.system_live_path,
686                DEFAULT_SYSTEM_LIVE_PATH.to_string()
687            );
688        });
689    }
690
691    #[test]
692    fn test_system_health_endpoint_path_custom() {
693        temp_env::with_vars(
694            vec![("DYN_SYSTEM_HEALTH_PATH", Some("/custom/health"))],
695            || {
696                let config = RuntimeConfig::from_settings().unwrap();
697                assert_eq!(config.system_health_path, "/custom/health");
698            },
699        );
700
701        temp_env::with_vars(vec![("DYN_SYSTEM_LIVE_PATH", Some("/custom/live"))], || {
702            let config = RuntimeConfig::from_settings().unwrap();
703            assert_eq!(config.system_live_path, "/custom/live");
704        });
705    }
706
707    #[test]
708    fn test_is_truthy_and_falsey() {
709        // Test truthy values
710        assert!(is_truthy("1"));
711        assert!(is_truthy("true"));
712        assert!(is_truthy("TRUE"));
713        assert!(is_truthy("on"));
714        assert!(is_truthy("yes"));
715
716        // Test falsey values
717        assert!(is_falsey("0"));
718        assert!(is_falsey("false"));
719        assert!(is_falsey("FALSE"));
720        assert!(is_falsey("off"));
721        assert!(is_falsey("no"));
722
723        // Test opposite behavior
724        assert!(!is_truthy("0"));
725        assert!(!is_falsey("1"));
726
727        // Test env functions
728        temp_env::with_vars(vec![("TEST_TRUTHY", Some("true"))], || {
729            assert!(env_is_truthy("TEST_TRUTHY"));
730            assert!(!env_is_falsey("TEST_TRUTHY"));
731        });
732
733        temp_env::with_vars(vec![("TEST_FALSEY", Some("false"))], || {
734            assert!(!env_is_truthy("TEST_FALSEY"));
735            assert!(env_is_falsey("TEST_FALSEY"));
736        });
737
738        // Test missing env vars
739        temp_env::with_vars(vec![("TEST_MISSING", None::<&str>)], || {
740            assert!(!env_is_truthy("TEST_MISSING"));
741            assert!(!env_is_falsey("TEST_MISSING"));
742        });
743    }
744
745    #[test]
746    fn test_request_plane_mode_from_str() {
747        assert_eq!(
748            "nats".parse::<RequestPlaneMode>().unwrap(),
749            RequestPlaneMode::Nats
750        );
751        assert_eq!(
752            "http".parse::<RequestPlaneMode>().unwrap(),
753            RequestPlaneMode::Http
754        );
755        assert_eq!(
756            "tcp".parse::<RequestPlaneMode>().unwrap(),
757            RequestPlaneMode::Tcp
758        );
759        assert_eq!(
760            "NATS".parse::<RequestPlaneMode>().unwrap(),
761            RequestPlaneMode::Nats
762        );
763        assert_eq!(
764            "HTTP".parse::<RequestPlaneMode>().unwrap(),
765            RequestPlaneMode::Http
766        );
767        assert_eq!(
768            "TCP".parse::<RequestPlaneMode>().unwrap(),
769            RequestPlaneMode::Tcp
770        );
771        assert!("invalid".parse::<RequestPlaneMode>().is_err());
772    }
773
774    #[test]
775    fn test_request_plane_mode_display() {
776        assert_eq!(RequestPlaneMode::Nats.to_string(), "nats");
777        assert_eq!(RequestPlaneMode::Http.to_string(), "http");
778        assert_eq!(RequestPlaneMode::Tcp.to_string(), "tcp");
779    }
780
781    #[test]
782    fn test_request_plane_mode_default() {
783        assert_eq!(RequestPlaneMode::default(), RequestPlaneMode::Nats);
784    }
785
786    #[test]
787    fn test_request_plane_mode_get_cached() {
788        // Test that get() returns a consistent value
789        let mode1 = RequestPlaneMode::get();
790        let mode2 = RequestPlaneMode::get();
791        assert_eq!(mode1, mode2, "Cached mode should be consistent");
792
793        // Verify it's one of the valid modes
794        assert!(
795            matches!(
796                mode1,
797                RequestPlaneMode::Nats | RequestPlaneMode::Http | RequestPlaneMode::Tcp
798            ),
799            "Mode should be a valid variant"
800        );
801    }
802}