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//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use super::Result;
17use derive_builder::Builder;
18use figment::{
19    providers::{Env, Format, Serialized, Toml},
20    Figment,
21};
22use serde::{Deserialize, Serialize};
23use validator::Validate;
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct WorkerConfig {
27    /// Grace shutdown period for http-service.
28    pub graceful_shutdown_timeout: u64,
29}
30
31impl WorkerConfig {
32    /// Instantiates and reads server configurations from appropriate sources.
33    /// Panics on invalid configuration.
34    pub fn from_settings() -> Self {
35        // All calls should be global and thread safe.
36        Figment::new()
37            .merge(Serialized::defaults(Self::default()))
38            .merge(Env::prefixed("DYN_WORKER_"))
39            .extract()
40            .unwrap() // safety: Called on startup, so panic is reasonable
41    }
42}
43
44impl Default for WorkerConfig {
45    fn default() -> Self {
46        WorkerConfig {
47            graceful_shutdown_timeout: if cfg!(debug_assertions) {
48                1 // Debug build: 1 second
49            } else {
50                30 // Release build: 30 seconds
51            },
52        }
53    }
54}
55
56/// Runtime configuration
57/// Defines the configuration for Tokio runtimes
58#[derive(Serialize, Deserialize, Validate, Debug, Builder, Clone)]
59#[builder(build_fn(private, name = "build_internal"), derive(Debug, Serialize))]
60pub struct RuntimeConfig {
61    /// Number of async worker threads
62    /// If set to 1, the runtime will run in single-threaded mode
63    #[validate(range(min = 1))]
64    #[builder(default = "16")]
65    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
66    pub num_worker_threads: usize,
67
68    /// Maximum number of blocking threads
69    /// Blocking threads are used for blocking operations, this value must be greater than 0.
70    #[validate(range(min = 1))]
71    #[builder(default = "512")]
72    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
73    pub max_blocking_threads: usize,
74}
75
76impl RuntimeConfig {
77    pub fn builder() -> RuntimeConfigBuilder {
78        RuntimeConfigBuilder::default()
79    }
80
81    pub(crate) fn figment() -> Figment {
82        Figment::new()
83            .merge(Serialized::defaults(RuntimeConfig::default()))
84            .merge(Toml::file("/opt/dynamo/defaults/runtime.toml"))
85            .merge(Toml::file("/opt/dynamo/etc/runtime.toml"))
86            .merge(Env::prefixed("DYN_RUNTIME_").filter_map(|k| {
87                let full_key = format!("DYN_RUNTIME_{}", k.as_str());
88                // filters out empty environment variables
89                match std::env::var(&full_key) {
90                    Ok(v) if !v.is_empty() => Some(k.into()),
91                    _ => None,
92                }
93            }))
94    }
95
96    /// Load the runtime configuration from the environment and configuration files
97    /// Configuration is priorities in the following order, where the last has the lowest priority:
98    /// 1. Environment variables (top priority)
99    ///    TO DO: Add documentation for configuration files. Paths should be configurable.
100    /// 2. /opt/dynamo/etc/runtime.toml
101    /// 3. /opt/dynamo/defaults/runtime.toml (lowest priority)
102    ///
103    /// Environment variables are prefixed with `DYN_RUNTIME_`
104    pub fn from_settings() -> Result<RuntimeConfig> {
105        let config: RuntimeConfig = Self::figment().extract()?;
106        config.validate()?;
107        Ok(config)
108    }
109
110    pub fn single_threaded() -> Self {
111        RuntimeConfig {
112            num_worker_threads: 1,
113            max_blocking_threads: 1,
114        }
115    }
116
117    /// Create a new default runtime configuration
118    pub(crate) fn create_runtime(&self) -> Result<tokio::runtime::Runtime> {
119        Ok(tokio::runtime::Builder::new_multi_thread()
120            .worker_threads(self.num_worker_threads)
121            .max_blocking_threads(self.max_blocking_threads)
122            .enable_all()
123            .build()?)
124    }
125}
126
127impl Default for RuntimeConfig {
128    fn default() -> Self {
129        Self {
130            num_worker_threads: 16,
131            max_blocking_threads: 16,
132        }
133    }
134}
135
136impl RuntimeConfigBuilder {
137    /// Build and validate the runtime configuration
138    pub fn build(&self) -> Result<RuntimeConfig> {
139        let config = self.build_internal()?;
140        config.validate()?;
141        Ok(config)
142    }
143}
144
145/// Check if an environment variable is truthy
146pub fn env_is_truthy(env: &str) -> bool {
147    match std::env::var(env) {
148        Ok(val) => is_truthy(val.as_str()),
149        Err(_) => false,
150    }
151}
152
153/// Check if a string is truthy
154/// This will be used to evaluate environment variables or any other subjective
155/// configuration parameters that can be set by the user that should be evaluated
156/// as a boolean value.
157pub fn is_truthy(val: &str) -> bool {
158    matches!(val.to_lowercase().as_str(), "1" | "true" | "on" | "yes")
159}
160
161/// Check whether JSONL logging enabled
162/// Set the `DYN_LOGGING_JSONL` environment variable a [`is_truthy`] value
163pub fn jsonl_logging_enabled() -> bool {
164    env_is_truthy("DYN_LOGGING_JSONL")
165}
166
167/// Check whether logging with ANSI terminal escape codes and colors is disabled.
168/// Set the `DYN_SDK_DISABLE_ANSI_LOGGING` environment variable a [`is_truthy`] value
169pub fn disable_ansi_logging() -> bool {
170    env_is_truthy("DYN_SDK_DISABLE_ANSI_LOGGING")
171}
172
173/// Check whether to use local timezone for logging timestamps (default is UTC)
174/// Set the `DYN_LOG_USE_LOCAL_TZ` environment variable to a [`is_truthy`] value
175pub fn use_local_timezone() -> bool {
176    env_is_truthy("DYN_LOG_USE_LOCAL_TZ")
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn test_runtime_config_with_env_vars() -> Result<()> {
185        temp_env::with_vars(
186            vec![
187                ("DYN_RUNTIME_NUM_WORKER_THREADS", Some("24")),
188                ("DYN_RUNTIME_MAX_BLOCKING_THREADS", Some("32")),
189            ],
190            || {
191                let config = RuntimeConfig::from_settings()?;
192                assert_eq!(config.num_worker_threads, 24);
193                assert_eq!(config.max_blocking_threads, 32);
194                Ok(())
195            },
196        )
197    }
198
199    #[test]
200    fn test_runtime_config_defaults() -> Result<()> {
201        temp_env::with_vars(
202            vec![
203                ("DYN_RUNTIME_NUM_WORKER_THREADS", None::<&str>),
204                ("DYN_RUNTIME_MAX_BLOCKING_THREADS", Some("")),
205            ],
206            || {
207                let config = RuntimeConfig::from_settings()?;
208
209                let default_config = RuntimeConfig::default();
210                assert_eq!(config.num_worker_threads, default_config.num_worker_threads);
211                assert_eq!(
212                    config.max_blocking_threads,
213                    default_config.max_blocking_threads
214                );
215                Ok(())
216            },
217        )
218    }
219
220    #[test]
221    fn test_runtime_config_rejects_invalid_thread_count() -> Result<()> {
222        temp_env::with_vars(
223            vec![
224                ("DYN_RUNTIME_NUM_WORKER_THREADS", Some("0")),
225                ("DYN_RUNTIME_MAX_BLOCKING_THREADS", Some("0")),
226            ],
227            || {
228                let result = RuntimeConfig::from_settings();
229                assert!(result.is_err());
230                if let Err(e) = result {
231                    assert!(e
232                        .to_string()
233                        .contains("num_worker_threads: Validation error"));
234                    assert!(e
235                        .to_string()
236                        .contains("max_blocking_threads: Validation error"));
237                }
238                Ok(())
239            },
240        )
241    }
242}