Skip to main content

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