config.rs 13.7 KB
Newer Older
1
2
// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
Ryan Olson's avatar
Ryan Olson committed
3
4
5
6
7
8
9
10

use super::Result;
use derive_builder::Builder;
use figment::{
    providers::{Env, Format, Serialized, Toml},
    Figment,
};
use serde::{Deserialize, Serialize};
11
use std::fmt;
Ryan Olson's avatar
Ryan Olson committed
12
13
use validator::Validate;

14
15
16
17
18
19
/// Default HTTP server host
const DEFAULT_HTTP_SERVER_HOST: &str = "0.0.0.0";

/// Default HTTP server port
const DEFAULT_HTTP_SERVER_PORT: u16 = 9090;

Ryan Olson's avatar
Ryan Olson committed
20
21
22
23
24
25
26
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkerConfig {
    /// Grace shutdown period for http-service.
    pub graceful_shutdown_timeout: u64,
}

impl WorkerConfig {
27
28
    /// Instantiates and reads server configurations from appropriate sources.
    /// Panics on invalid configuration.
Ryan Olson's avatar
Ryan Olson committed
29
30
31
32
    pub fn from_settings() -> Self {
        // All calls should be global and thread safe.
        Figment::new()
            .merge(Serialized::defaults(Self::default()))
33
            .merge(Env::prefixed("DYN_WORKER_"))
Ryan Olson's avatar
Ryan Olson committed
34
            .extract()
35
            .unwrap() // safety: Called on startup, so panic is reasonable
Ryan Olson's avatar
Ryan Olson committed
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
    }
}

impl Default for WorkerConfig {
    fn default() -> Self {
        WorkerConfig {
            graceful_shutdown_timeout: if cfg!(debug_assertions) {
                1 // Debug build: 1 second
            } else {
                30 // Release build: 30 seconds
            },
        }
    }
}

/// Runtime configuration
/// Defines the configuration for Tokio runtimes
#[derive(Serialize, Deserialize, Validate, Debug, Builder, Clone)]
#[builder(build_fn(private, name = "build_internal"), derive(Debug, Serialize))]
pub struct RuntimeConfig {
56
    /// Number of async worker threads
Ryan Olson's avatar
Ryan Olson committed
57
    /// If set to 1, the runtime will run in single-threaded mode
58
59
    /// Set this at runtime with environment variable DYN_RUNTIME_NUM_WORKER_THREADS. Defaults to
    /// number of cores.
Ryan Olson's avatar
Ryan Olson committed
60
61
    #[validate(range(min = 1))]
    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
62
    pub num_worker_threads: Option<usize>,
Ryan Olson's avatar
Ryan Olson committed
63
64
65

    /// Maximum number of blocking threads
    /// Blocking threads are used for blocking operations, this value must be greater than 0.
66
67
    /// Set this at runtime with environment variable DYN_RUNTIME_MAX_BLOCKING_THREADS. Defaults to
    /// 512.
Ryan Olson's avatar
Ryan Olson committed
68
    #[validate(range(min = 1))]
69
    #[builder(default = "512")]
Ryan Olson's avatar
Ryan Olson committed
70
71
    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
    pub max_blocking_threads: usize,
72
73

    /// HTTP server host for health and metrics endpoints
74
    /// Set this at runtime with environment variable DYN_RUNTIME_HTTP_SERVER_HOST
75
76
77
78
79
80
    #[builder(default = "DEFAULT_HTTP_SERVER_HOST.to_string()")]
    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
    pub http_server_host: String,

    /// HTTP server port for health and metrics endpoints
    /// If set to 0, the system will assign a random available port
81
    /// Set this at runtime with environment variable DYN_RUNTIME_HTTP_SERVER_PORT
82
83
84
85
86
    #[builder(default = "DEFAULT_HTTP_SERVER_PORT")]
    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
    pub http_server_port: u16,

    /// Health and metrics HTTP server enabled
87
    /// Set this at runtime with environment variable DYN_RUNTIME_HTTP_ENABLED
88
89
90
    #[builder(default = "false")]
    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
    pub http_enabled: bool,
Ryan Olson's avatar
Ryan Olson committed
91
92
}

93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
impl fmt::Display for RuntimeConfig {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // If None, it defaults to "number of cores", so we indicate that.
        match self.num_worker_threads {
            Some(val) => write!(f, "num_worker_threads={val}, ")?,
            None => write!(f, "num_worker_threads=default (num_cores), ")?,
        }

        write!(f, "max_blocking_threads={}, ", self.max_blocking_threads)?;
        write!(f, "http_server_host={}, ", self.http_server_host)?;
        write!(f, "http_server_port={}, ", self.http_server_port)?;
        write!(f, "http_enabled={}", self.http_enabled)?;

        Ok(())
    }
}

Ryan Olson's avatar
Ryan Olson committed
110
111
112
113
114
115
116
117
impl RuntimeConfig {
    pub fn builder() -> RuntimeConfigBuilder {
        RuntimeConfigBuilder::default()
    }

    pub(crate) fn figment() -> Figment {
        Figment::new()
            .merge(Serialized::defaults(RuntimeConfig::default()))
Neelay Shah's avatar
Neelay Shah committed
118
119
            .merge(Toml::file("/opt/dynamo/defaults/runtime.toml"))
            .merge(Toml::file("/opt/dynamo/etc/runtime.toml"))
120
121
            .merge(Env::prefixed("DYN_RUNTIME_").filter_map(|k| {
                let full_key = format!("DYN_RUNTIME_{}", k.as_str());
122
123
124
125
126
127
                // filters out empty environment variables
                match std::env::var(&full_key) {
                    Ok(v) if !v.is_empty() => Some(k.into()),
                    _ => None,
                }
            }))
Ryan Olson's avatar
Ryan Olson committed
128
129
130
131
132
    }

    /// Load the runtime configuration from the environment and configuration files
    /// Configuration is priorities in the following order, where the last has the lowest priority:
    /// 1. Environment variables (top priority)
133
    ///    TO DO: Add documentation for configuration files. Paths should be configurable.
Neelay Shah's avatar
Neelay Shah committed
134
135
    /// 2. /opt/dynamo/etc/runtime.toml
    /// 3. /opt/dynamo/defaults/runtime.toml (lowest priority)
Ryan Olson's avatar
Ryan Olson committed
136
    ///
137
    /// Environment variables are prefixed with `DYN_RUNTIME_`
Ryan Olson's avatar
Ryan Olson committed
138
139
140
141
142
143
    pub fn from_settings() -> Result<RuntimeConfig> {
        let config: RuntimeConfig = Self::figment().extract()?;
        config.validate()?;
        Ok(config)
    }

144
145
146
147
148
149
150
    /// Check if HTTP server should be enabled
    /// HTTP server is enabled by default, but can be disabled by setting DYN_RUNTIME_HTTP_ENABLED to false
    /// If a port is explicitly provided, HTTP server will be enabled regardless
    pub fn http_server_enabled(&self) -> bool {
        self.http_enabled
    }

Ryan Olson's avatar
Ryan Olson committed
151
152
    pub fn single_threaded() -> Self {
        RuntimeConfig {
153
            num_worker_threads: Some(1),
Ryan Olson's avatar
Ryan Olson committed
154
            max_blocking_threads: 1,
155
156
157
            http_server_host: DEFAULT_HTTP_SERVER_HOST.to_string(),
            http_server_port: DEFAULT_HTTP_SERVER_PORT,
            http_enabled: false,
Ryan Olson's avatar
Ryan Olson committed
158
159
160
161
        }
    }

    /// Create a new default runtime configuration
162
163
164
165
166
167
    pub(crate) fn create_runtime(&self) -> std::io::Result<tokio::runtime::Runtime> {
        tokio::runtime::Builder::new_multi_thread()
            .worker_threads(
                self.num_worker_threads
                    .unwrap_or_else(|| std::thread::available_parallelism().unwrap().get()),
            )
Ryan Olson's avatar
Ryan Olson committed
168
169
            .max_blocking_threads(self.max_blocking_threads)
            .enable_all()
170
            .build()
Ryan Olson's avatar
Ryan Olson committed
171
172
173
174
175
    }
}

impl Default for RuntimeConfig {
    fn default() -> Self {
176
        let num_cores = std::thread::available_parallelism().unwrap().get();
177
        Self {
178
179
            num_worker_threads: Some(num_cores),
            max_blocking_threads: num_cores,
180
181
182
            http_server_host: DEFAULT_HTTP_SERVER_HOST.to_string(),
            http_server_port: DEFAULT_HTTP_SERVER_PORT,
            http_enabled: false,
183
        }
Ryan Olson's avatar
Ryan Olson committed
184
185
186
187
188
189
190
191
192
193
194
    }
}

impl RuntimeConfigBuilder {
    /// Build and validate the runtime configuration
    pub fn build(&self) -> Result<RuntimeConfig> {
        let config = self.build_internal()?;
        config.validate()?;
        Ok(config)
    }
}
195

196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
/// Check if a string is truthy
/// This will be used to evaluate environment variables or any other subjective
/// configuration parameters that can be set by the user that should be evaluated
/// as a boolean value.
pub fn is_truthy(val: &str) -> bool {
    matches!(val.to_lowercase().as_str(), "1" | "true" | "on" | "yes")
}

/// Check if a string is falsey
/// This will be used to evaluate environment variables or any other subjective
/// configuration parameters that can be set by the user that should be evaluated
/// as a boolean value (opposite of is_truthy).
pub fn is_falsey(val: &str) -> bool {
    matches!(val.to_lowercase().as_str(), "0" | "false" | "off" | "no")
}

212
213
214
215
216
217
218
219
/// Check if an environment variable is truthy
pub fn env_is_truthy(env: &str) -> bool {
    match std::env::var(env) {
        Ok(val) => is_truthy(val.as_str()),
        Err(_) => false,
    }
}

220
221
222
223
224
225
/// Check if an environment variable is falsey
pub fn env_is_falsey(env: &str) -> bool {
    match std::env::var(env) {
        Ok(val) => is_falsey(val.as_str()),
        Err(_) => false,
    }
226
227
228
}

/// Check whether JSONL logging enabled
229
/// Set the `DYN_LOGGING_JSONL` environment variable a [`is_truthy`] value
230
pub fn jsonl_logging_enabled() -> bool {
231
    env_is_truthy("DYN_LOGGING_JSONL")
232
233
234
}

/// Check whether logging with ANSI terminal escape codes and colors is disabled.
235
/// Set the `DYN_SDK_DISABLE_ANSI_LOGGING` environment variable a [`is_truthy`] value
236
pub fn disable_ansi_logging() -> bool {
237
    env_is_truthy("DYN_SDK_DISABLE_ANSI_LOGGING")
238
}
239

Ryan Olson's avatar
Ryan Olson committed
240
241
242
243
244
245
/// Check whether to use local timezone for logging timestamps (default is UTC)
/// Set the `DYN_LOG_USE_LOCAL_TZ` environment variable to a [`is_truthy`] value
pub fn use_local_timezone() -> bool {
    env_is_truthy("DYN_LOG_USE_LOCAL_TZ")
}

246
247
248
249
250
251
252
253
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_runtime_config_with_env_vars() -> Result<()> {
        temp_env::with_vars(
            vec![
254
255
                ("DYN_RUNTIME_NUM_WORKER_THREADS", Some("24")),
                ("DYN_RUNTIME_MAX_BLOCKING_THREADS", Some("32")),
256
257
258
            ],
            || {
                let config = RuntimeConfig::from_settings()?;
259
                assert_eq!(config.num_worker_threads, Some(24));
260
261
262
263
264
265
266
267
268
269
                assert_eq!(config.max_blocking_threads, 32);
                Ok(())
            },
        )
    }

    #[test]
    fn test_runtime_config_defaults() -> Result<()> {
        temp_env::with_vars(
            vec![
270
271
                ("DYN_RUNTIME_NUM_WORKER_THREADS", None::<&str>),
                ("DYN_RUNTIME_MAX_BLOCKING_THREADS", Some("")),
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
            ],
            || {
                let config = RuntimeConfig::from_settings()?;

                let default_config = RuntimeConfig::default();
                assert_eq!(config.num_worker_threads, default_config.num_worker_threads);
                assert_eq!(
                    config.max_blocking_threads,
                    default_config.max_blocking_threads
                );
                Ok(())
            },
        )
    }

    #[test]
    fn test_runtime_config_rejects_invalid_thread_count() -> Result<()> {
        temp_env::with_vars(
            vec![
291
292
                ("DYN_RUNTIME_NUM_WORKER_THREADS", Some("0")),
                ("DYN_RUNTIME_MAX_BLOCKING_THREADS", Some("0")),
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
            ],
            || {
                let result = RuntimeConfig::from_settings();
                assert!(result.is_err());
                if let Err(e) = result {
                    assert!(e
                        .to_string()
                        .contains("num_worker_threads: Validation error"));
                    assert!(e
                        .to_string()
                        .contains("max_blocking_threads: Validation error"));
                }
                Ok(())
            },
        )
    }
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395

    #[test]
    fn test_runtime_config_http_server_env_vars() -> Result<()> {
        temp_env::with_vars(
            vec![
                ("DYN_RUNTIME_HTTP_SERVER_HOST", Some("127.0.0.1")),
                ("DYN_RUNTIME_HTTP_SERVER_PORT", Some("9090")),
            ],
            || {
                let config = RuntimeConfig::from_settings()?;
                assert_eq!(config.http_server_host, "127.0.0.1");
                assert_eq!(config.http_server_port, 9090);
                Ok(())
            },
        )
    }

    #[test]
    fn test_http_server_enabled_by_default() {
        temp_env::with_vars(vec![("DYN_RUNTIME_HTTP_ENABLED", None::<&str>)], || {
            let config = RuntimeConfig::from_settings().unwrap();
            assert!(!config.http_server_enabled());
        });
    }

    #[test]
    fn test_http_server_disabled_explicitly() {
        temp_env::with_vars(vec![("DYN_RUNTIME_HTTP_ENABLED", Some("false"))], || {
            let config = RuntimeConfig::from_settings().unwrap();
            assert!(!config.http_server_enabled());
        });
    }

    #[test]
    fn test_http_server_enabled_explicitly() {
        temp_env::with_vars(vec![("DYN_RUNTIME_HTTP_ENABLED", Some("true"))], || {
            let config = RuntimeConfig::from_settings().unwrap();
            assert!(config.http_server_enabled());
        });
    }

    #[test]
    fn test_http_server_enabled_by_port() {
        temp_env::with_vars(vec![("DYN_RUNTIME_HTTP_SERVER_PORT", Some("8080"))], || {
            let config = RuntimeConfig::from_settings().unwrap();
            assert!(!config.http_server_enabled());
            assert_eq!(config.http_server_port, 8080);
        });
    }

    #[test]
    fn test_is_truthy_and_falsey() {
        // Test truthy values
        assert!(is_truthy("1"));
        assert!(is_truthy("true"));
        assert!(is_truthy("TRUE"));
        assert!(is_truthy("on"));
        assert!(is_truthy("yes"));

        // Test falsey values
        assert!(is_falsey("0"));
        assert!(is_falsey("false"));
        assert!(is_falsey("FALSE"));
        assert!(is_falsey("off"));
        assert!(is_falsey("no"));

        // Test opposite behavior
        assert!(!is_truthy("0"));
        assert!(!is_falsey("1"));

        // Test env functions
        temp_env::with_vars(vec![("TEST_TRUTHY", Some("true"))], || {
            assert!(env_is_truthy("TEST_TRUTHY"));
            assert!(!env_is_falsey("TEST_TRUTHY"));
        });

        temp_env::with_vars(vec![("TEST_FALSEY", Some("false"))], || {
            assert!(!env_is_truthy("TEST_FALSEY"));
            assert!(env_is_falsey("TEST_FALSEY"));
        });

        // Test missing env vars
        temp_env::with_vars(vec![("TEST_MISSING", None::<&str>)], || {
            assert!(!env_is_truthy("TEST_MISSING"));
            assert!(!env_is_falsey("TEST_MISSING"));
        });
    }
396
}