1use 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#[doc(hidden)]
16pub mod env_config;
17pub mod environment_names;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum ParserVersion {
22 Auto,
23 V1,
24 V2,
25}
26
27impl TryFrom<Option<&str>> for ParserVersion {
28 type Error = anyhow::Error;
29
30 fn try_from(value: Option<&str>) -> Result<Self> {
31 use environment_names::llm::DYN_PARSER_VERSION;
32 match value {
33 Some("1") => Ok(Self::V1),
34 Some("2") => Ok(Self::V2),
35 Some("auto") | None => Ok(Self::Auto),
36 Some(value) => {
37 anyhow::bail!("{DYN_PARSER_VERSION} must be unset, auto, 1, or 2; got {value:?}")
38 }
39 }
40 }
41}
42
43pub fn selected_parser_version() -> Result<ParserVersion> {
45 use environment_names::llm::{DYN_ENABLE_EXPERIMENTAL_PARSERS_V2, DYN_PARSER_VERSION};
46 static VERSION: OnceLock<Result<ParserVersion, String>> = OnceLock::new();
47 VERSION
48 .get_or_init(|| {
49 let version = match std::env::var(DYN_PARSER_VERSION) {
50 Ok(value) => ParserVersion::try_from(Some(value.as_str())),
51 Err(std::env::VarError::NotPresent) => ParserVersion::try_from(None),
52 Err(std::env::VarError::NotUnicode(_)) => Err(anyhow::anyhow!(
53 "{DYN_PARSER_VERSION} must be unset, auto, 1, or 2; value is not valid UTF-8"
54 )),
55 };
56 let experimental = std::env::var_os(DYN_ENABLE_EXPERIMENTAL_PARSERS_V2);
57 let enabled = experimental
58 .as_ref()
59 .map(|value| {
60 let value = value.to_str().ok_or_else(|| {
61 anyhow::anyhow!("value is not valid UTF-8")
62 })?;
63 parse_bool(value).map_err(|error| {
64 anyhow::anyhow!("{DYN_ENABLE_EXPERIMENTAL_PARSERS_V2}={value:?} is not a valid boolean: {error}")
65 })
66 })
67 .transpose();
68
69 let selection = match (enabled, version) {
70 (Ok(Some(true)), Ok(ParserVersion::V1))
71 | (Ok(Some(false)), Ok(ParserVersion::V2)) => Err(anyhow::anyhow!(
72 "conflicting parser settings: {DYN_ENABLE_EXPERIMENTAL_PARSERS_V2}={:?} conflicts with {DYN_PARSER_VERSION}={:?}",
73 experimental.as_ref().map(|value| value.to_string_lossy()),
74 std::env::var_os(DYN_PARSER_VERSION)
75 .map(|value| value.to_string_lossy().into_owned())
76 )),
77 (Err(error), _) => Err(error),
78 (Ok(_), Err(error)) => Err(error),
79 (Ok(Some(true)), Ok(ParserVersion::Auto)) => Ok(ParserVersion::V2),
80 (Ok(Some(false)) | Ok(None), Ok(ParserVersion::Auto)) => Ok(ParserVersion::Auto),
81 (Ok(Some(false)), Ok(ParserVersion::V1)) => Ok(ParserVersion::V1),
82 (Ok(Some(true)), Ok(ParserVersion::V2)) => Ok(ParserVersion::V2),
83 (Ok(None), Ok(ParserVersion::V1)) => Ok(ParserVersion::V1),
84 (Ok(None), Ok(ParserVersion::V2)) => Ok(ParserVersion::V2),
85 };
86
87 if let Some(raw_value) = experimental.as_ref() {
88 let value = raw_value.to_str().unwrap_or("<non-UTF-8>");
89 let resolved = selection
90 .as_ref()
91 .map(|selection| format!("{selection:?}"))
92 .unwrap_or_else(|_| "invalid or conflicting settings".to_string());
93 tracing::warn!(
94 target: "dynamo_unified",
95 value,
96 resolved_selection = %resolved,
97 "experimental V2 parser routing setting is present"
98 );
99 }
100
101 selection.map_err(|error| error.to_string())
102 })
103 .as_ref()
104 .copied()
105 .map_err(|error| anyhow::anyhow!(error.clone()))
106}
107
108const DEFAULT_SYSTEM_HOST: &str = "0.0.0.0";
110
111const DEFAULT_SYSTEM_PORT: i16 = -1;
113
114const DEFAULT_SYSTEM_HEALTH_PATH: &str = "/health";
116const DEFAULT_SYSTEM_LIVE_PATH: &str = "/live";
117
118pub const DEFAULT_CANARY_WAIT_TIME_SECS: u64 = 10;
121pub const DEFAULT_HEALTH_CHECK_REQUEST_TIMEOUT_SECS: u64 = 3;
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct WorkerConfig {
126 pub graceful_shutdown_timeout: u64,
128}
129
130impl WorkerConfig {
131 pub fn from_settings() -> Self {
134 Figment::new()
136 .merge(Serialized::defaults(Self::default()))
137 .merge(Env::prefixed("DYN_WORKER_"))
138 .extract()
139 .unwrap() }
141}
142
143impl Default for WorkerConfig {
144 fn default() -> Self {
145 WorkerConfig {
146 graceful_shutdown_timeout: if cfg!(debug_assertions) {
147 1 } else {
149 30 },
151 }
152 }
153}
154
155#[derive(Debug, Deserialize, Serialize, PartialEq, Clone)]
156#[serde(rename_all = "lowercase")]
157pub enum HealthStatus {
158 Ready,
159 NotReady,
160}
161
162#[derive(Serialize, Deserialize, Validate, Debug, Builder, Clone)]
165#[builder(build_fn(private, name = "build_internal"), derive(Debug, Serialize))]
166pub struct RuntimeConfig {
167 #[validate(range(min = 1))]
172 #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
173 pub num_worker_threads: Option<usize>,
174
175 #[validate(range(min = 1))]
185 #[builder(default = "512")]
186 #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
187 pub max_blocking_threads: usize,
188
189 #[builder(default = "DEFAULT_SYSTEM_HOST.to_string()")]
192 #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
193 pub system_host: String,
194
195 #[builder(default = "DEFAULT_SYSTEM_PORT")]
201 #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
202 pub system_port: i16,
203
204 #[deprecated(
208 note = "Use system_port instead. Set DYN_SYSTEM_PORT to enable the system metrics server."
209 )]
210 #[builder(default = "false")]
211 #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
212 pub system_enabled: bool,
213
214 #[builder(default = "HealthStatus::NotReady")]
217 #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
218 pub starting_health_status: HealthStatus,
219
220 #[builder(default = "vec![]")]
226 #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
227 pub use_endpoint_health_status: Vec<String>,
228
229 #[builder(default = "DEFAULT_SYSTEM_HEALTH_PATH.to_string()")]
232 #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
233 pub system_health_path: String,
234 #[builder(default = "DEFAULT_SYSTEM_LIVE_PATH.to_string()")]
236 #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
237 pub system_live_path: String,
238
239 #[builder(default = "None")]
243 #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
244 pub compute_threads: Option<usize>,
245
246 #[builder(default = "Some(2 * 1024 * 1024)")]
250 #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
251 pub compute_stack_size: Option<usize>,
252
253 #[builder(default = "\"compute\".to_string()")]
256 #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
257 pub compute_thread_prefix: String,
258
259 #[builder(default = "false")]
262 #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
263 pub health_check_enabled: bool,
264
265 #[builder(default = "DEFAULT_CANARY_WAIT_TIME_SECS")]
268 #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
269 pub canary_wait_time_secs: u64,
270
271 #[builder(default = "DEFAULT_HEALTH_CHECK_REQUEST_TIMEOUT_SECS")]
274 #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
275 pub health_check_request_timeout_secs: u64,
276}
277
278impl fmt::Display for RuntimeConfig {
279 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
280 match self.num_worker_threads {
282 Some(val) => write!(f, "num_worker_threads={val}, ")?,
283 None => write!(f, "num_worker_threads=default (num_cores), ")?,
284 }
285
286 write!(f, "max_blocking_threads={}, ", self.max_blocking_threads)?;
287 write!(f, "system_host={}, ", self.system_host)?;
288 write!(f, "system_port={}, ", self.system_port)?;
289 write!(
290 f,
291 "use_endpoint_health_status={:?}",
292 self.use_endpoint_health_status
293 )?;
294 write!(
295 f,
296 "starting_health_status={:?}",
297 self.starting_health_status
298 )?;
299 write!(f, ", system_health_path={}", self.system_health_path)?;
300 write!(f, ", system_live_path={}", self.system_live_path)?;
301 write!(f, ", health_check_enabled={}", self.health_check_enabled)?;
302 write!(f, ", canary_wait_time_secs={}", self.canary_wait_time_secs)?;
303 write!(
304 f,
305 ", health_check_request_timeout_secs={}",
306 self.health_check_request_timeout_secs
307 )?;
308
309 Ok(())
310 }
311}
312
313impl RuntimeConfig {
314 pub fn builder() -> RuntimeConfigBuilder {
315 RuntimeConfigBuilder::default()
316 }
317
318 pub(crate) fn figment() -> Figment {
319 Figment::new()
320 .merge(Serialized::defaults(RuntimeConfig::default()))
321 .merge(Toml::file("/opt/dynamo/defaults/runtime.toml"))
322 .merge(Toml::file("/opt/dynamo/etc/runtime.toml"))
323 .merge(Env::prefixed("DYN_RUNTIME_").filter_map(|k| {
324 let full_key = format!("DYN_RUNTIME_{}", k.as_str());
325 match std::env::var(&full_key) {
327 Ok(v) if !v.is_empty() => Some(k.into()),
328 _ => None,
329 }
330 }))
331 .merge(Env::prefixed("DYN_SYSTEM_").filter_map(|k| {
332 let full_key = format!("DYN_SYSTEM_{}", k.as_str());
333 match std::env::var(&full_key) {
335 Ok(v) if !v.is_empty() => {
336 let mapped_key = match k.as_str() {
338 "HOST" => "system_host",
339 "PORT" => "system_port",
340 "ENABLED" => "system_enabled",
341 "USE_ENDPOINT_HEALTH_STATUS" => "use_endpoint_health_status",
342 "STARTING_HEALTH_STATUS" => "starting_health_status",
343 "HEALTH_PATH" => "system_health_path",
344 "LIVE_PATH" => "system_live_path",
345 _ => k.as_str(),
346 };
347 Some(mapped_key.into())
348 }
349 _ => None,
350 }
351 }))
352 .merge(Env::prefixed("DYN_COMPUTE_").filter_map(|k| {
353 let full_key = format!("DYN_COMPUTE_{}", k.as_str());
354 match std::env::var(&full_key) {
356 Ok(v) if !v.is_empty() => {
357 let mapped_key = match k.as_str() {
359 "THREADS" => "compute_threads",
360 "STACK_SIZE" => "compute_stack_size",
361 "THREAD_PREFIX" => "compute_thread_prefix",
362 _ => k.as_str(),
363 };
364 Some(mapped_key.into())
365 }
366 _ => None,
367 }
368 }))
369 .merge(Env::prefixed("DYN_HEALTH_CHECK_").filter_map(|k| {
370 let full_key = format!("DYN_HEALTH_CHECK_{}", k.as_str());
371 match std::env::var(&full_key) {
373 Ok(v) if !v.is_empty() => {
374 let mapped_key = match k.as_str() {
376 "ENABLED" => "health_check_enabled",
377 "REQUEST_TIMEOUT" => "health_check_request_timeout_secs",
378 _ => k.as_str(),
379 };
380 Some(mapped_key.into())
381 }
382 _ => None,
383 }
384 }))
385 .merge(Env::prefixed("DYN_CANARY_").filter_map(|k| {
386 let full_key = format!("DYN_CANARY_{}", k.as_str());
387 match std::env::var(&full_key) {
389 Ok(v) if !v.is_empty() => {
390 let mapped_key = match k.as_str() {
392 "WAIT_TIME" => "canary_wait_time_secs",
393 _ => k.as_str(),
394 };
395 Some(mapped_key.into())
396 }
397 _ => None,
398 }
399 }))
400 }
401
402 pub fn from_settings() -> Result<RuntimeConfig> {
411 use environment_names::runtime::system as env_system;
412 Self::validate_parser_environment()?;
413 if std::env::var(env_system::DYN_SYSTEM_USE_ENDPOINT_HEALTH_STATUS).is_ok() {
415 tracing::warn!(
416 "DYN_SYSTEM_USE_ENDPOINT_HEALTH_STATUS is deprecated and no longer used. \
417 System health is now determined by endpoints that register with health check payloads. \
418 Please update your configuration to register health check payloads directly on endpoints."
419 );
420 }
421
422 if std::env::var(env_system::DYN_SYSTEM_ENABLED).is_ok() {
423 tracing::warn!(
424 "DYN_SYSTEM_ENABLED is deprecated. \
425 System metrics server is now controlled solely by DYN_SYSTEM_PORT. \
426 Set DYN_SYSTEM_PORT to a positive value to enable the server, or set to -1 to disable (default)."
427 );
428 }
429
430 let config: RuntimeConfig = Self::figment().extract()?;
431 config.validate()?;
432 Ok(config)
433 }
434
435 pub fn validate_parser_environment() -> Result<()> {
436 selected_parser_version().map(|_| ())
437 }
438
439 pub fn system_server_enabled(&self) -> bool {
444 self.system_port >= 0
445 }
446
447 pub fn single_threaded() -> Self {
448 RuntimeConfig {
449 num_worker_threads: Some(1),
450 max_blocking_threads: 1,
451 system_host: DEFAULT_SYSTEM_HOST.to_string(),
452 system_port: DEFAULT_SYSTEM_PORT,
453 #[allow(deprecated)]
454 system_enabled: false,
455 starting_health_status: HealthStatus::NotReady,
456 use_endpoint_health_status: vec![],
457 system_health_path: DEFAULT_SYSTEM_HEALTH_PATH.to_string(),
458 system_live_path: DEFAULT_SYSTEM_LIVE_PATH.to_string(),
459 compute_threads: Some(1),
460 compute_stack_size: Some(2 * 1024 * 1024),
461 compute_thread_prefix: "compute".to_string(),
462 health_check_enabled: false,
463 canary_wait_time_secs: DEFAULT_CANARY_WAIT_TIME_SECS,
464 health_check_request_timeout_secs: DEFAULT_HEALTH_CHECK_REQUEST_TIMEOUT_SECS,
465 }
466 }
467
468 pub fn tokio_builder(&self) -> tokio::runtime::Builder {
475 let mut builder = tokio::runtime::Builder::new_multi_thread();
476 builder
477 .worker_threads(
478 self.num_worker_threads
479 .unwrap_or_else(|| std::thread::available_parallelism().unwrap().get()),
480 )
481 .max_blocking_threads(self.max_blocking_threads)
482 .enable_all();
483 if env_is_truthy(environment_names::runtime::DYN_ENABLE_POLL_HISTOGRAM) {
484 tracing::info!(
485 "Tokio poll-time histogram enabled (DYN_ENABLE_POLL_HISTOGRAM); \
486 expect ~2× Instant::now() overhead per task poll"
487 );
488 builder.enable_metrics_poll_time_histogram();
489 }
490 builder
491 }
492
493 pub(crate) fn create_runtime(&self) -> std::io::Result<tokio::runtime::Runtime> {
495 self.tokio_builder().build()
496 }
497}
498
499impl Default for RuntimeConfig {
500 fn default() -> Self {
501 let num_cores = std::thread::available_parallelism().unwrap().get();
502 Self {
503 num_worker_threads: Some(num_cores),
504 max_blocking_threads: num_cores,
505 system_host: DEFAULT_SYSTEM_HOST.to_string(),
506 system_port: DEFAULT_SYSTEM_PORT,
507 #[allow(deprecated)]
508 system_enabled: false,
509 starting_health_status: HealthStatus::NotReady,
510 use_endpoint_health_status: vec![],
511 system_health_path: DEFAULT_SYSTEM_HEALTH_PATH.to_string(),
512 system_live_path: DEFAULT_SYSTEM_LIVE_PATH.to_string(),
513 compute_threads: None,
514 compute_stack_size: Some(2 * 1024 * 1024),
515 compute_thread_prefix: "compute".to_string(),
516 health_check_enabled: false,
517 canary_wait_time_secs: DEFAULT_CANARY_WAIT_TIME_SECS,
518 health_check_request_timeout_secs: DEFAULT_HEALTH_CHECK_REQUEST_TIMEOUT_SECS,
519 }
520 }
521}
522
523impl RuntimeConfigBuilder {
524 pub fn build(&self) -> Result<RuntimeConfig> {
526 let config = self.build_internal()?;
527 config.validate()?;
528 Ok(config)
529 }
530}
531
532pub use dynamo_truthy::{
538 env_is_falsey, env_is_truthy, is_falsey, is_truthy, parse_bool, parse_bool_opt,
539};
540
541#[derive(Debug, Clone, Copy, PartialEq, Eq)]
542pub enum ConsoleLogFormat {
543 Readable,
544 Jsonl,
545}
546
547impl ConsoleLogFormat {
548 fn from_env_value(value: &str) -> Option<Self> {
549 match value.to_ascii_lowercase().as_str() {
550 "readable" => Some(Self::Readable),
551 "jsonl" => Some(Self::Jsonl),
552 _ => None,
553 }
554 }
555
556 pub fn as_str(self) -> &'static str {
557 match self {
558 Self::Readable => "readable",
559 Self::Jsonl => "jsonl",
560 }
561 }
562}
563
564pub(crate) fn legacy_jsonl_logging_enabled() -> bool {
569 env_is_truthy(environment_names::logging::DYN_LOGGING_JSONL)
570}
571
572pub fn console_log_format() -> ConsoleLogFormat {
577 let legacy_format = || {
578 if legacy_jsonl_logging_enabled() {
579 ConsoleLogFormat::Jsonl
580 } else {
581 ConsoleLogFormat::Readable
582 }
583 };
584
585 match std::env::var(environment_names::logging::DYN_LOGGING_CONSOLE_FORMAT) {
586 Ok(value) if value.trim().is_empty() => legacy_format(),
587 Ok(value) => match ConsoleLogFormat::from_env_value(value.trim()) {
588 Some(format) => format,
589 None => {
590 eprintln!(
591 "Invalid {} value '{}'; using readable console logs",
592 environment_names::logging::DYN_LOGGING_CONSOLE_FORMAT,
593 value
594 );
595 ConsoleLogFormat::Readable
596 }
597 },
598 Err(_) => legacy_format(),
599 }
600}
601
602pub fn jsonl_logging_enabled() -> bool {
604 console_log_format() == ConsoleLogFormat::Jsonl
605}
606
607pub fn disable_ansi_logging() -> bool {
610 env_is_truthy(environment_names::logging::DYN_SDK_DISABLE_ANSI_LOGGING)
611}
612
613pub fn use_local_timezone() -> bool {
616 env_is_truthy(environment_names::logging::DYN_LOG_USE_LOCAL_TZ)
617}
618
619pub fn span_events_enabled() -> bool {
621 env_is_truthy(environment_names::logging::DYN_LOGGING_SPAN_EVENTS)
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627
628 #[test]
629 fn test_runtime_config_builder_overrides_related_fields() -> Result<()> {
630 let config = RuntimeConfig::builder()
631 .num_worker_threads(Some(24))
632 .max_blocking_threads(32)
633 .system_host("127.0.0.1".to_string())
634 .system_port(9090)
635 .build()?;
636
637 assert_eq!(config.num_worker_threads, Some(24));
638 assert_eq!(config.max_blocking_threads, 32);
639 assert_eq!(config.system_host, "127.0.0.1");
640 assert_eq!(config.system_port, 9090);
641 Ok(())
642 }
643
644 #[test]
651 fn test_from_settings_reads_both_thread_env_vars() {
652 const WORKERS: &str = "DYN_RUNTIME_NUM_WORKER_THREADS";
653 const BLOCKING: &str = "DYN_RUNTIME_MAX_BLOCKING_THREADS";
654
655 temp_env::with_vars([(WORKERS, Some("7")), (BLOCKING, Some("11"))], || {
656 let config = RuntimeConfig::from_settings().expect("from_settings failed");
657 assert_eq!(config.num_worker_threads, Some(7), "{WORKERS} was not read");
658 assert_eq!(config.max_blocking_threads, 11, "{BLOCKING} was not read");
659 });
660 }
661
662 #[test]
663 fn parser_selection_configuration_table() {
664 const CHILD: &str = "DYNAMO_PARSER_SELECTION_TEST_CHILD";
665 const EXPECTED: &str = "DYNAMO_PARSER_SELECTION_EXPECTED";
666 use environment_names::llm::{DYN_ENABLE_EXPERIMENTAL_PARSERS_V2, DYN_PARSER_VERSION};
667
668 if std::env::var_os(CHILD).is_some() {
669 let expected = std::env::var(EXPECTED).expect("expected selection");
670 let result = selected_parser_version();
671 match expected.as_str() {
672 "Auto" => assert_eq!(result.unwrap(), ParserVersion::Auto),
673 "V1" => assert_eq!(result.unwrap(), ParserVersion::V1),
674 "V2" => assert_eq!(result.unwrap(), ParserVersion::V2),
675 "conflict" => {
676 let error = result.unwrap_err().to_string();
677 assert!(error.contains(DYN_ENABLE_EXPERIMENTAL_PARSERS_V2));
678 assert!(error.contains(DYN_PARSER_VERSION));
679 }
680 "invalid" => {
681 assert!(result.is_err());
682 assert!(RuntimeConfig::from_settings().is_err());
683 }
684 other => panic!("unexpected expected selection: {other}"),
685 }
686 return;
687 }
688
689 let cases = [
690 (None, None, "Auto"),
691 (Some("false"), None, "Auto"),
692 (Some("0"), None, "Auto"),
693 (Some("off"), None, "Auto"),
694 (Some("no"), None, "Auto"),
695 (Some(""), None, "Auto"),
696 (Some("true"), None, "V2"),
697 (Some("1"), None, "V2"),
698 (Some("on"), None, "V2"),
699 (Some("yes"), None, "V2"),
700 (None, Some("auto"), "Auto"),
701 (None, Some("1"), "V1"),
702 (None, Some("v1"), "invalid"),
703 (None, Some("2"), "V2"),
704 (None, Some("v2"), "invalid"),
705 (Some("false"), Some("auto"), "Auto"),
706 (Some("true"), Some("auto"), "V2"),
707 (Some("false"), Some("1"), "V1"),
708 (Some("false"), Some("2"), "conflict"),
709 (Some("true"), Some("1"), "conflict"),
710 (Some("true"), Some("2"), "V2"),
711 (Some("false"), Some("v1"), "invalid"),
712 (Some("false"), Some("v2"), "invalid"),
713 (Some("true"), Some("v1"), "invalid"),
714 (Some("true"), Some("v2"), "invalid"),
715 (Some("perhaps"), None, "invalid"),
716 (None, Some(""), "invalid"),
717 (None, Some("3"), "invalid"),
718 ];
719 for (experimental, version, expected) in cases {
720 let mut command = crate::test_utils::isolated_command(
721 "config::tests::parser_selection_configuration_table",
722 );
723 command.env(CHILD, "1").env(EXPECTED, expected).env(
724 DYN_ENABLE_EXPERIMENTAL_PARSERS_V2,
725 experimental.unwrap_or(""),
726 );
727 if experimental.is_none() {
728 command.env_remove(DYN_ENABLE_EXPERIMENTAL_PARSERS_V2);
729 }
730 if let Some(version) = version {
731 command.env(DYN_PARSER_VERSION, version);
732 } else {
733 command.env_remove(DYN_PARSER_VERSION);
734 }
735 let output = command
736 .output()
737 .expect("run isolated parser selection test");
738 crate::test_utils::assert_isolated_success(&output);
739 }
740 }
741
742 #[test]
743 fn parser_selection_is_cached_for_process_lifetime() {
744 const CHILD: &str = "DYNAMO_PARSER_SELECTION_CACHE_CHILD";
745 if std::env::var_os(CHILD).is_none() {
746 let output = crate::test_utils::isolated_command(
747 "config::tests::parser_selection_is_cached_for_process_lifetime",
748 )
749 .env(CHILD, "1")
750 .env(
751 environment_names::llm::DYN_ENABLE_EXPERIMENTAL_PARSERS_V2,
752 "true",
753 )
754 .output()
755 .expect("run isolated parser cache test");
756 crate::test_utils::assert_isolated_success(&output);
757 return;
758 }
759
760 assert_eq!(selected_parser_version().unwrap(), ParserVersion::V2);
761 temp_env::with_vars(
762 [
763 (
764 environment_names::llm::DYN_ENABLE_EXPERIMENTAL_PARSERS_V2,
765 None,
766 ),
767 (environment_names::llm::DYN_PARSER_VERSION, Some("1")),
768 ],
769 || {
770 assert_eq!(selected_parser_version().unwrap(), ParserVersion::V2);
771 },
772 );
773 }
774
775 #[test]
776 fn parser_selection_warning_is_emitted_once_and_startup_rejects_conflicts() {
777 const CHILD: &str = "DYNAMO_PARSER_SELECTION_WARNING_CHILD";
778 const MODE: &str = "DYNAMO_PARSER_SELECTION_WARNING_MODE";
779 if std::env::var_os(CHILD).is_none() {
780 let warning = crate::test_utils::isolated_command(
781 "config::tests::parser_selection_warning_is_emitted_once_and_startup_rejects_conflicts",
782 )
783 .env(CHILD, "1")
784 .env(MODE, "warning")
785 .env(environment_names::llm::DYN_ENABLE_EXPERIMENTAL_PARSERS_V2, "true")
786 .output()
787 .expect("run isolated parser warning test");
788 crate::test_utils::assert_isolated_success(&warning);
789 let stderr = String::from_utf8_lossy(&warning.stderr);
790 assert_eq!(
791 stderr
792 .matches("experimental V2 parser routing setting is present")
793 .count(),
794 1,
795 "{stderr}"
796 );
797 assert!(stderr.contains("value=\"true\""), "{stderr}");
798 assert!(stderr.contains("resolved_selection=V2"), "{stderr}");
799
800 let conflict = crate::test_utils::isolated_command(
801 "config::tests::parser_selection_warning_is_emitted_once_and_startup_rejects_conflicts",
802 )
803 .env(CHILD, "1")
804 .env(MODE, "conflict")
805 .env(environment_names::llm::DYN_ENABLE_EXPERIMENTAL_PARSERS_V2, "true")
806 .env(environment_names::llm::DYN_PARSER_VERSION, "1")
807 .output()
808 .expect("run isolated parser startup conflict test");
809 crate::test_utils::assert_isolated_success(&conflict);
810 let stderr = String::from_utf8_lossy(&conflict.stderr);
811 assert_eq!(
812 stderr
813 .matches("experimental V2 parser routing setting is present")
814 .count(),
815 1,
816 "{stderr}"
817 );
818 return;
819 }
820
821 match std::env::var(MODE).as_deref() {
822 Ok("warning") => {
823 let subscriber = tracing_subscriber::fmt()
824 .with_ansi(false)
825 .with_writer(std::io::stderr)
826 .finish();
827 tracing::subscriber::with_default(subscriber, || {
828 assert_eq!(selected_parser_version().unwrap(), ParserVersion::V2);
829 assert_eq!(selected_parser_version().unwrap(), ParserVersion::V2);
830 });
831 }
832 Ok("conflict") => {
833 let subscriber = tracing_subscriber::fmt()
834 .with_ansi(false)
835 .with_writer(std::io::stderr)
836 .finish();
837 tracing::subscriber::with_default(subscriber, || {
838 let error = RuntimeConfig::from_settings().unwrap_err().to_string();
839 assert!(error.contains("DYN_ENABLE_EXPERIMENTAL_PARSERS_V2"));
840 assert!(error.contains("DYN_PARSER_VERSION"));
841 });
842 }
843 value => panic!("unexpected test mode: {value:?}"),
844 }
845 }
846
847 #[test]
853 fn test_tokio_builder_applies_configured_worker_threads() -> Result<()> {
854 let config = RuntimeConfig::builder()
855 .num_worker_threads(Some(3))
856 .max_blocking_threads(5)
857 .build()?;
858
859 let runtime = config.tokio_builder().build()?;
860 assert_eq!(runtime.metrics().num_workers(), 3);
861 Ok(())
862 }
863
864 #[test]
866 fn test_tokio_builder_defaults_worker_threads_to_core_count() -> Result<()> {
867 let config = RuntimeConfig {
868 num_worker_threads: None,
869 ..RuntimeConfig::default()
870 };
871
872 let runtime = config.tokio_builder().build()?;
873 assert_eq!(
874 runtime.metrics().num_workers(),
875 std::thread::available_parallelism()?.get()
876 );
877 Ok(())
878 }
879
880 #[test]
892 fn test_tokio_builder_applies_max_blocking_threads() -> Result<()> {
893 use std::sync::Arc;
894 use std::sync::atomic::{AtomicUsize, Ordering};
895
896 const CAP: usize = 2;
897
898 let config = RuntimeConfig::builder()
899 .num_worker_threads(Some(2))
900 .max_blocking_threads(CAP)
901 .build()?;
902 let runtime = config.tokio_builder().build()?;
903
904 let in_flight = Arc::new(AtomicUsize::new(0));
905 let peak = Arc::new(AtomicUsize::new(0));
906
907 runtime.block_on(async {
908 let tasks: Vec<_> = (0..CAP * 4)
909 .map(|_| {
910 let in_flight = Arc::clone(&in_flight);
911 let peak = Arc::clone(&peak);
912 tokio::task::spawn_blocking(move || {
913 let now = in_flight.fetch_add(1, Ordering::SeqCst) + 1;
914 peak.fetch_max(now, Ordering::SeqCst);
915 std::thread::sleep(std::time::Duration::from_millis(20));
917 in_flight.fetch_sub(1, Ordering::SeqCst);
918 })
919 })
920 .collect();
921
922 for task in tasks {
923 task.await.expect("blocking task panicked");
924 }
925 });
926
927 let observed = peak.load(Ordering::SeqCst);
928 assert!(
929 observed <= CAP,
930 "{observed} blocking tasks ran at once, but the cap was {CAP}"
931 );
932 Ok(())
933 }
934
935 #[test]
938 fn test_default_max_blocking_threads_is_core_count() {
939 let cores = std::thread::available_parallelism().unwrap().get();
940 let config = RuntimeConfig::default();
941 assert_eq!(config.max_blocking_threads, cores);
942 assert_eq!(config.num_worker_threads, Some(cores));
943 }
944
945 #[test]
946 fn test_runtime_config_rejects_invalid_thread_count() -> Result<()> {
947 let result = RuntimeConfig::builder()
948 .num_worker_threads(Some(0))
949 .max_blocking_threads(0)
950 .build();
951
952 let error = result.unwrap_err().to_string();
953 assert!(error.contains("num_worker_threads: Validation error"));
954 assert!(error.contains("max_blocking_threads: Validation error"));
955 Ok(())
956 }
957
958 #[test]
959 fn test_system_server_enabled_by_nonnegative_port() {
960 let mut config = RuntimeConfig::default();
961 for (port, enabled) in [(-1, false), (0, true), (9527, true)] {
962 config.system_port = port;
963 assert_eq!(config.system_server_enabled(), enabled);
964 }
965 }
966
967 #[test]
968 fn test_is_truthy_and_falsey() {
969 assert!(is_truthy("1"));
971 assert!(is_truthy("true"));
972 assert!(is_truthy("TRUE"));
973 assert!(is_truthy("on"));
974 assert!(is_truthy("yes"));
975
976 assert!(is_falsey("0"));
978 assert!(is_falsey("false"));
979 assert!(is_falsey("FALSE"));
980 assert!(is_falsey("off"));
981 assert!(is_falsey("no"));
982
983 assert!(!is_truthy("0"));
985 assert!(!is_falsey("1"));
986 }
987
988 #[test]
989 fn test_console_log_format() {
990 use environment_names::logging;
991
992 for (console_format, legacy_jsonl, expected) in [
993 (None, None, ConsoleLogFormat::Readable),
994 (None, Some("true"), ConsoleLogFormat::Jsonl),
995 (Some(""), Some("true"), ConsoleLogFormat::Jsonl),
996 (Some(" "), Some("true"), ConsoleLogFormat::Jsonl),
997 (Some(" jsonl "), Some("false"), ConsoleLogFormat::Jsonl),
998 (Some("readable"), Some("true"), ConsoleLogFormat::Readable),
999 (Some("jsonl"), Some("false"), ConsoleLogFormat::Jsonl),
1000 (
1001 Some("unsupported"),
1002 Some("true"),
1003 ConsoleLogFormat::Readable,
1004 ),
1005 ] {
1006 temp_env::with_vars(
1007 [
1008 (logging::DYN_LOGGING_CONSOLE_FORMAT, console_format),
1009 (logging::DYN_LOGGING_JSONL, legacy_jsonl),
1010 ],
1011 || {
1012 assert_eq!(console_log_format(), expected);
1013 assert_eq!(jsonl_logging_enabled(), expected == ConsoleLogFormat::Jsonl);
1014 },
1015 );
1016 }
1017 }
1018}