".github/vscode:/vscode.git/clone" did not exist on "088295e00750c2807a576603680cf143d2acf5af"
config.rs 12.9 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
Ryan Olson's avatar
Ryan Olson committed
15
16
17
18
19
20
21
22
23
24

use super::Result;
use derive_builder::Builder;
use figment::{
    providers::{Env, Format, Serialized, Toml},
    Figment,
};
use serde::{Deserialize, Serialize};
use validator::Validate;

25
26
27
28
29
30
/// 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
31
32
33
34
35
36
37
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkerConfig {
    /// Grace shutdown period for http-service.
    pub graceful_shutdown_timeout: u64,
}

impl WorkerConfig {
38
39
    /// Instantiates and reads server configurations from appropriate sources.
    /// Panics on invalid configuration.
Ryan Olson's avatar
Ryan Olson committed
40
41
42
43
    pub fn from_settings() -> Self {
        // All calls should be global and thread safe.
        Figment::new()
            .merge(Serialized::defaults(Self::default()))
44
            .merge(Env::prefixed("DYN_WORKER_"))
Ryan Olson's avatar
Ryan Olson committed
45
            .extract()
46
            .unwrap() // safety: Called on startup, so panic is reasonable
Ryan Olson's avatar
Ryan Olson committed
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
    }
}

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 {
67
    /// Number of async worker threads
Ryan Olson's avatar
Ryan Olson committed
68
69
70
71
    /// If set to 1, the runtime will run in single-threaded mode
    #[validate(range(min = 1))]
    #[builder(default = "16")]
    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
72
    pub num_worker_threads: usize,
Ryan Olson's avatar
Ryan Olson committed
73
74
75
76

    /// Maximum number of blocking threads
    /// Blocking threads are used for blocking operations, this value must be greater than 0.
    #[validate(range(min = 1))]
77
    #[builder(default = "512")]
Ryan Olson's avatar
Ryan Olson committed
78
79
    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
    pub max_blocking_threads: usize,
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95

    /// HTTP server host for health and metrics endpoints
    #[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
    #[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
    #[builder(default = "false")]
    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
    pub http_enabled: bool,
Ryan Olson's avatar
Ryan Olson committed
96
97
98
99
100
101
102
103
104
105
}

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
106
107
            .merge(Toml::file("/opt/dynamo/defaults/runtime.toml"))
            .merge(Toml::file("/opt/dynamo/etc/runtime.toml"))
108
109
            .merge(Env::prefixed("DYN_RUNTIME_").filter_map(|k| {
                let full_key = format!("DYN_RUNTIME_{}", k.as_str());
110
111
112
113
114
115
                // 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
116
117
118
119
120
    }

    /// 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)
121
    ///    TO DO: Add documentation for configuration files. Paths should be configurable.
Neelay Shah's avatar
Neelay Shah committed
122
123
    /// 2. /opt/dynamo/etc/runtime.toml
    /// 3. /opt/dynamo/defaults/runtime.toml (lowest priority)
Ryan Olson's avatar
Ryan Olson committed
124
    ///
125
    /// Environment variables are prefixed with `DYN_RUNTIME_`
Ryan Olson's avatar
Ryan Olson committed
126
127
128
129
130
131
    pub fn from_settings() -> Result<RuntimeConfig> {
        let config: RuntimeConfig = Self::figment().extract()?;
        config.validate()?;
        Ok(config)
    }

132
133
134
135
136
137
138
    /// 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
139
140
    pub fn single_threaded() -> Self {
        RuntimeConfig {
141
            num_worker_threads: 1,
Ryan Olson's avatar
Ryan Olson committed
142
            max_blocking_threads: 1,
143
144
145
            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
146
147
148
149
150
151
        }
    }

    /// Create a new default runtime configuration
    pub(crate) fn create_runtime(&self) -> Result<tokio::runtime::Runtime> {
        Ok(tokio::runtime::Builder::new_multi_thread()
152
            .worker_threads(self.num_worker_threads)
Ryan Olson's avatar
Ryan Olson committed
153
154
155
156
157
158
159
160
            .max_blocking_threads(self.max_blocking_threads)
            .enable_all()
            .build()?)
    }
}

impl Default for RuntimeConfig {
    fn default() -> Self {
161
        Self {
162
            num_worker_threads: 16,
163
            max_blocking_threads: 16,
164
165
166
            http_server_host: DEFAULT_HTTP_SERVER_HOST.to_string(),
            http_server_port: DEFAULT_HTTP_SERVER_PORT,
            http_enabled: false,
167
        }
Ryan Olson's avatar
Ryan Olson committed
168
169
170
171
172
173
174
175
176
177
178
    }
}

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

180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
/// 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")
}

196
197
198
199
200
201
202
203
/// 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,
    }
}

204
205
206
207
208
209
/// 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,
    }
210
211
212
}

/// Check whether JSONL logging enabled
213
/// Set the `DYN_LOGGING_JSONL` environment variable a [`is_truthy`] value
214
pub fn jsonl_logging_enabled() -> bool {
215
    env_is_truthy("DYN_LOGGING_JSONL")
216
217
218
}

/// Check whether logging with ANSI terminal escape codes and colors is disabled.
219
/// Set the `DYN_SDK_DISABLE_ANSI_LOGGING` environment variable a [`is_truthy`] value
220
pub fn disable_ansi_logging() -> bool {
221
    env_is_truthy("DYN_SDK_DISABLE_ANSI_LOGGING")
222
}
223

Ryan Olson's avatar
Ryan Olson committed
224
225
226
227
228
229
/// 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")
}

230
231
232
233
234
235
236
237
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_runtime_config_with_env_vars() -> Result<()> {
        temp_env::with_vars(
            vec![
238
239
                ("DYN_RUNTIME_NUM_WORKER_THREADS", Some("24")),
                ("DYN_RUNTIME_MAX_BLOCKING_THREADS", Some("32")),
240
241
242
243
244
245
246
247
248
249
250
251
252
253
            ],
            || {
                let config = RuntimeConfig::from_settings()?;
                assert_eq!(config.num_worker_threads, 24);
                assert_eq!(config.max_blocking_threads, 32);
                Ok(())
            },
        )
    }

    #[test]
    fn test_runtime_config_defaults() -> Result<()> {
        temp_env::with_vars(
            vec![
254
255
                ("DYN_RUNTIME_NUM_WORKER_THREADS", None::<&str>),
                ("DYN_RUNTIME_MAX_BLOCKING_THREADS", Some("")),
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
            ],
            || {
                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![
275
276
                ("DYN_RUNTIME_NUM_WORKER_THREADS", Some("0")),
                ("DYN_RUNTIME_MAX_BLOCKING_THREADS", Some("0")),
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
            ],
            || {
                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(())
            },
        )
    }
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
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

    #[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"));
        });
    }
380
}