execution_engine/
config.rs

1//! Execution Engine Configuration
2//!
3//! This module defines configuration for the ExecutionEngine.
4//! See docs/configuration.md for complete specification.
5
6use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8
9/// Strategy for handling output that exceeds max_output_size_bytes
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11pub enum OversizedOutputStrategy {
12    /// Truncate output and add warning message
13    TruncateWithWarning,
14    /// Fail the execution with error
15    FailExecution,
16    /// Stream to temporary file instead of memory
17    StreamToFile,
18}
19
20/// Configuration for ExecutionEngine
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ExecutionConfig {
23    /// Default timeout in milliseconds (used when request doesn't specify)
24    pub default_timeout_ms: u64,
25
26    /// Maximum allowed timeout in milliseconds (requests cannot exceed this)
27    pub max_timeout_ms: u64,
28
29    /// Whether to stream output line-by-line (true) or buffer until complete (false)
30    pub stream_output: bool,
31
32    /// Directory for execution logs (None = no logging)
33    pub log_dir: Option<PathBuf>,
34
35    /// Maximum number of concurrent executions (semaphore limit)
36    pub max_concurrent_executions: usize,
37
38    /// Maximum number of executions to keep in memory
39    pub max_in_memory_executions: usize,
40
41    /// How long to retain completed executions in memory (seconds)
42    pub execution_retention_secs: u64,
43
44    /// Whether to automatically run cleanup task
45    pub enable_auto_cleanup: bool,
46
47    /// Maximum output size in bytes before applying strategy
48    pub max_output_size_bytes: usize,
49
50    /// Strategy for handling output that exceeds max_output_size_bytes
51    pub oversized_output_strategy: OversizedOutputStrategy,
52}
53
54impl Default for ExecutionConfig {
55    fn default() -> Self {
56        Self {
57            default_timeout_ms: 300_000, // 5 minutes
58            max_timeout_ms: 3_600_000,   // 1 hour
59            stream_output: true,
60            log_dir: None,
61            max_concurrent_executions: 100,
62            max_in_memory_executions: 1_000,
63            execution_retention_secs: 3_600, // 1 hour
64            enable_auto_cleanup: true,
65            max_output_size_bytes: 10_485_760, // 10 MB
66            oversized_output_strategy: OversizedOutputStrategy::TruncateWithWarning,
67        }
68    }
69}
70
71impl ExecutionConfig {
72    /// Validate configuration values
73    ///
74    /// Returns an error message if configuration is invalid, None otherwise.
75    ///
76    /// # Errors
77    ///
78    /// Returns `Err` with a descriptive message if:
79    /// - `default_timeout_ms` is 0
80    /// - `max_timeout_ms` is 0
81    /// - `default_timeout_ms` exceeds `max_timeout_ms`
82    /// - `max_concurrent_executions` is 0
83    /// - `max_in_memory_executions` is 0
84    /// - `execution_retention_secs` is 0
85    /// - `max_output_size_bytes` is 0
86    pub fn validate(&self) -> Result<(), String> {
87        if self.default_timeout_ms == 0 {
88            return Err("default_timeout_ms must be greater than 0".to_string());
89        }
90
91        if self.max_timeout_ms == 0 {
92            return Err("max_timeout_ms must be greater than 0".to_string());
93        }
94
95        if self.default_timeout_ms > self.max_timeout_ms {
96            return Err("default_timeout_ms cannot exceed max_timeout_ms".to_string());
97        }
98
99        if self.max_concurrent_executions == 0 {
100            return Err("max_concurrent_executions must be greater than 0".to_string());
101        }
102
103        if self.max_in_memory_executions == 0 {
104            return Err("max_in_memory_executions must be greater than 0".to_string());
105        }
106
107        if self.execution_retention_secs == 0 {
108            return Err("execution_retention_secs must be greater than 0".to_string());
109        }
110
111        if self.max_output_size_bytes == 0 {
112            return Err("max_output_size_bytes must be greater than 0".to_string());
113        }
114
115        Ok(())
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn test_default_config() {
125        let config = ExecutionConfig::default();
126        assert_eq!(config.default_timeout_ms, 300_000);
127        assert_eq!(config.max_timeout_ms, 3_600_000);
128        assert!(config.stream_output);
129        assert!(config.log_dir.is_none());
130        assert_eq!(config.max_concurrent_executions, 100);
131        assert_eq!(config.max_in_memory_executions, 1_000);
132        assert_eq!(config.execution_retention_secs, 3_600);
133        assert!(config.enable_auto_cleanup);
134        assert_eq!(config.max_output_size_bytes, 10_485_760);
135        assert_eq!(
136            config.oversized_output_strategy,
137            OversizedOutputStrategy::TruncateWithWarning
138        );
139    }
140
141    #[test]
142    fn test_validate_success() {
143        let config = ExecutionConfig::default();
144        assert!(config.validate().is_ok());
145    }
146
147    #[test]
148    fn test_validate_default_timeout_zero() {
149        let mut config = ExecutionConfig::default();
150        config.default_timeout_ms = 0;
151        assert!(config.validate().is_err());
152        assert_eq!(
153            config.validate().unwrap_err(),
154            "default_timeout_ms must be greater than 0"
155        );
156    }
157
158    #[test]
159    fn test_validate_max_timeout_zero() {
160        let mut config = ExecutionConfig::default();
161        config.max_timeout_ms = 0;
162        assert!(config.validate().is_err());
163        assert_eq!(
164            config.validate().unwrap_err(),
165            "max_timeout_ms must be greater than 0"
166        );
167    }
168
169    #[test]
170    fn test_validate_default_exceeds_max() {
171        let mut config = ExecutionConfig::default();
172        config.default_timeout_ms = 1_000_000;
173        config.max_timeout_ms = 500_000;
174        assert!(config.validate().is_err());
175        assert_eq!(
176            config.validate().unwrap_err(),
177            "default_timeout_ms cannot exceed max_timeout_ms"
178        );
179    }
180
181    #[test]
182    fn test_validate_max_concurrent_zero() {
183        let mut config = ExecutionConfig::default();
184        config.max_concurrent_executions = 0;
185        assert!(config.validate().is_err());
186        assert_eq!(
187            config.validate().unwrap_err(),
188            "max_concurrent_executions must be greater than 0"
189        );
190    }
191
192    #[test]
193    fn test_validate_max_in_memory_zero() {
194        let mut config = ExecutionConfig::default();
195        config.max_in_memory_executions = 0;
196        assert!(config.validate().is_err());
197        assert_eq!(
198            config.validate().unwrap_err(),
199            "max_in_memory_executions must be greater than 0"
200        );
201    }
202
203    #[test]
204    fn test_validate_retention_zero() {
205        let mut config = ExecutionConfig::default();
206        config.execution_retention_secs = 0;
207        assert!(config.validate().is_err());
208        assert_eq!(
209            config.validate().unwrap_err(),
210            "execution_retention_secs must be greater than 0"
211        );
212    }
213
214    #[test]
215    fn test_validate_max_output_size_zero() {
216        let mut config = ExecutionConfig::default();
217        config.max_output_size_bytes = 0;
218        assert!(config.validate().is_err());
219        assert_eq!(
220            config.validate().unwrap_err(),
221            "max_output_size_bytes must be greater than 0"
222        );
223    }
224
225    #[test]
226    fn test_oversized_output_strategy_equality() {
227        assert_eq!(
228            OversizedOutputStrategy::TruncateWithWarning,
229            OversizedOutputStrategy::TruncateWithWarning
230        );
231        assert_ne!(
232            OversizedOutputStrategy::TruncateWithWarning,
233            OversizedOutputStrategy::FailExecution
234        );
235        assert_ne!(
236            OversizedOutputStrategy::FailExecution,
237            OversizedOutputStrategy::StreamToFile
238        );
239    }
240
241    #[test]
242    fn test_config_clone() {
243        let config = ExecutionConfig::default();
244        let cloned = config.clone();
245        assert_eq!(config.default_timeout_ms, cloned.default_timeout_ms);
246        assert_eq!(config.max_timeout_ms, cloned.max_timeout_ms);
247    }
248}