escher-execution-engine 0.1.2

Production-ready async execution engine for system commands
Documentation
//! Execution Engine Configuration
//!
//! This module defines configuration for the ExecutionEngine.
//! See docs/configuration.md for complete specification.

use serde::{Deserialize, Serialize};
use std::path::PathBuf;

#[cfg(feature = "server-feedback")]
use std::sync::Arc;

#[cfg(feature = "server-feedback")]
use cloudops_network::NetworkClient;

/// Strategy for handling output that exceeds max_output_size_bytes
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum OversizedOutputStrategy {
    /// Truncate output and add warning message
    TruncateWithWarning,
    /// Fail the execution with error
    FailExecution,
    /// Stream to temporary file instead of memory
    StreamToFile,
}

/// Configuration for ExecutionEngine
#[derive(Clone)]
pub struct ExecutionConfig {
    /// Default timeout in milliseconds (used when request doesn't specify)
    pub default_timeout_ms: u64,

    /// Maximum allowed timeout in milliseconds (requests cannot exceed this)
    pub max_timeout_ms: u64,

    /// Whether to stream output line-by-line (true) or buffer until complete (false)
    pub stream_output: bool,

    /// Directory for execution logs (None = no logging)
    pub log_dir: Option<PathBuf>,

    /// Maximum number of concurrent executions (semaphore limit)
    pub max_concurrent_executions: usize,

    /// Maximum number of executions to keep in memory
    pub max_in_memory_executions: usize,

    /// How long to retain completed executions in memory (seconds)
    pub execution_retention_secs: u64,

    /// Whether to automatically run cleanup task
    pub enable_auto_cleanup: bool,

    /// Maximum output size in bytes before applying strategy
    pub max_output_size_bytes: usize,

    /// Strategy for handling output that exceeds max_output_size_bytes
    pub oversized_output_strategy: OversizedOutputStrategy,

    /// Whether to enable server feedback (requires server-feedback feature)
    pub enable_server_feedback: bool,

    /// Optional network client for sending execution feedback to server
    /// Only available when server-feedback feature is enabled
    #[cfg(feature = "server-feedback")]
    pub network_client: Option<Arc<NetworkClient>>,
}

impl std::fmt::Debug for ExecutionConfig {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ExecutionConfig")
            .field("default_timeout_ms", &self.default_timeout_ms)
            .field("max_timeout_ms", &self.max_timeout_ms)
            .field("stream_output", &self.stream_output)
            .field("log_dir", &self.log_dir)
            .field("max_concurrent_executions", &self.max_concurrent_executions)
            .field("max_in_memory_executions", &self.max_in_memory_executions)
            .field("execution_retention_secs", &self.execution_retention_secs)
            .field("enable_auto_cleanup", &self.enable_auto_cleanup)
            .field("max_output_size_bytes", &self.max_output_size_bytes)
            .field("oversized_output_strategy", &self.oversized_output_strategy)
            .field("enable_server_feedback", &self.enable_server_feedback)
            .finish()
    }
}

impl Default for ExecutionConfig {
    fn default() -> Self {
        Self {
            default_timeout_ms: 300_000, // 5 minutes
            max_timeout_ms: 3_600_000,   // 1 hour
            stream_output: true,
            log_dir: None,
            max_concurrent_executions: 100,
            max_in_memory_executions: 1_000,
            execution_retention_secs: 3_600, // 1 hour
            enable_auto_cleanup: true,
            max_output_size_bytes: 10_485_760, // 10 MB
            oversized_output_strategy: OversizedOutputStrategy::TruncateWithWarning,
            enable_server_feedback: false,
            #[cfg(feature = "server-feedback")]
            network_client: None,
        }
    }
}

impl ExecutionConfig {
    /// Validate configuration values
    ///
    /// Returns an error message if configuration is invalid, None otherwise.
    ///
    /// # Errors
    ///
    /// Returns `Err` with a descriptive message if:
    /// - `default_timeout_ms` is 0
    /// - `max_timeout_ms` is 0
    /// - `default_timeout_ms` exceeds `max_timeout_ms`
    /// - `max_concurrent_executions` is 0
    /// - `max_in_memory_executions` is 0
    /// - `execution_retention_secs` is 0
    /// - `max_output_size_bytes` is 0
    pub fn validate(&self) -> Result<(), String> {
        if self.default_timeout_ms == 0 {
            return Err("default_timeout_ms must be greater than 0".to_string());
        }

        if self.max_timeout_ms == 0 {
            return Err("max_timeout_ms must be greater than 0".to_string());
        }

        if self.default_timeout_ms > self.max_timeout_ms {
            return Err("default_timeout_ms cannot exceed max_timeout_ms".to_string());
        }

        if self.max_concurrent_executions == 0 {
            return Err("max_concurrent_executions must be greater than 0".to_string());
        }

        if self.max_in_memory_executions == 0 {
            return Err("max_in_memory_executions must be greater than 0".to_string());
        }

        if self.execution_retention_secs == 0 {
            return Err("execution_retention_secs must be greater than 0".to_string());
        }

        if self.max_output_size_bytes == 0 {
            return Err("max_output_size_bytes must be greater than 0".to_string());
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_default_config() {
        let config = ExecutionConfig::default();
        assert_eq!(config.default_timeout_ms, 300_000);
        assert_eq!(config.max_timeout_ms, 3_600_000);
        assert!(config.stream_output);
        assert!(config.log_dir.is_none());
        assert_eq!(config.max_concurrent_executions, 100);
        assert_eq!(config.max_in_memory_executions, 1_000);
        assert_eq!(config.execution_retention_secs, 3_600);
        assert!(config.enable_auto_cleanup);
        assert_eq!(config.max_output_size_bytes, 10_485_760);
        assert_eq!(
            config.oversized_output_strategy,
            OversizedOutputStrategy::TruncateWithWarning
        );
    }

    #[test]
    fn test_validate_success() {
        let config = ExecutionConfig::default();
        assert!(config.validate().is_ok());
    }

    #[test]
    fn test_validate_default_timeout_zero() {
        let mut config = ExecutionConfig::default();
        config.default_timeout_ms = 0;
        assert!(config.validate().is_err());
        assert_eq!(
            config.validate().unwrap_err(),
            "default_timeout_ms must be greater than 0"
        );
    }

    #[test]
    fn test_validate_max_timeout_zero() {
        let mut config = ExecutionConfig::default();
        config.max_timeout_ms = 0;
        assert!(config.validate().is_err());
        assert_eq!(
            config.validate().unwrap_err(),
            "max_timeout_ms must be greater than 0"
        );
    }

    #[test]
    fn test_validate_default_exceeds_max() {
        let mut config = ExecutionConfig::default();
        config.default_timeout_ms = 1_000_000;
        config.max_timeout_ms = 500_000;
        assert!(config.validate().is_err());
        assert_eq!(
            config.validate().unwrap_err(),
            "default_timeout_ms cannot exceed max_timeout_ms"
        );
    }

    #[test]
    fn test_validate_max_concurrent_zero() {
        let mut config = ExecutionConfig::default();
        config.max_concurrent_executions = 0;
        assert!(config.validate().is_err());
        assert_eq!(
            config.validate().unwrap_err(),
            "max_concurrent_executions must be greater than 0"
        );
    }

    #[test]
    fn test_validate_max_in_memory_zero() {
        let mut config = ExecutionConfig::default();
        config.max_in_memory_executions = 0;
        assert!(config.validate().is_err());
        assert_eq!(
            config.validate().unwrap_err(),
            "max_in_memory_executions must be greater than 0"
        );
    }

    #[test]
    fn test_validate_retention_zero() {
        let mut config = ExecutionConfig::default();
        config.execution_retention_secs = 0;
        assert!(config.validate().is_err());
        assert_eq!(
            config.validate().unwrap_err(),
            "execution_retention_secs must be greater than 0"
        );
    }

    #[test]
    fn test_validate_max_output_size_zero() {
        let mut config = ExecutionConfig::default();
        config.max_output_size_bytes = 0;
        assert!(config.validate().is_err());
        assert_eq!(
            config.validate().unwrap_err(),
            "max_output_size_bytes must be greater than 0"
        );
    }

    #[test]
    fn test_oversized_output_strategy_equality() {
        assert_eq!(
            OversizedOutputStrategy::TruncateWithWarning,
            OversizedOutputStrategy::TruncateWithWarning
        );
        assert_ne!(
            OversizedOutputStrategy::TruncateWithWarning,
            OversizedOutputStrategy::FailExecution
        );
        assert_ne!(
            OversizedOutputStrategy::FailExecution,
            OversizedOutputStrategy::StreamToFile
        );
    }

    #[test]
    fn test_config_clone() {
        let config = ExecutionConfig::default();
        let cloned = config.clone();
        assert_eq!(config.default_timeout_ms, cloned.default_timeout_ms);
        assert_eq!(config.max_timeout_ms, cloned.max_timeout_ms);
    }
}