387 lines
12 KiB
Rust
387 lines
12 KiB
Rust
//! Runtime Module
|
|
//!
|
|
//! Provides runtime abstraction and implementations for executing actions
|
|
//! in different environments. The primary runtime is `ProcessRuntime`, a
|
|
//! generic, configuration-driven runtime that reads its behavior from the
|
|
//! database `runtime.execution_config` JSONB column.
|
|
//!
|
|
//! Language-specific runtimes (Python, Node.js, etc.) are NOT implemented
|
|
//! as separate Rust types. Instead, the `ProcessRuntime` handles all
|
|
//! languages by using the interpreter, environment, and dependency
|
|
//! configuration stored in the database.
|
|
//!
|
|
//! ## Runtime Version Selection
|
|
//!
|
|
//! When an action declares a `runtime_version_constraint` (e.g., `">=3.12"`),
|
|
//! the executor resolves the best matching `RuntimeVersion` from the database
|
|
//! and passes its `execution_config` through `ExecutionContext::runtime_config_override`.
|
|
//! The `ProcessRuntime` uses this override instead of its built-in config,
|
|
//! enabling version-specific interpreter binaries, environment commands, etc.
|
|
//!
|
|
//! The environment directory is also overridden to include the version suffix
|
|
//! (e.g., `python-3.12` instead of `python`) so that different versions
|
|
//! maintain isolated environments.
|
|
|
|
pub mod dependency;
|
|
pub mod local;
|
|
pub mod log_writer;
|
|
pub mod native;
|
|
pub mod parameter_passing;
|
|
pub mod process;
|
|
pub mod process_executor;
|
|
|
|
// Re-export runtime implementations
|
|
pub use local::LocalRuntime;
|
|
pub use native::NativeRuntime;
|
|
pub use process::ProcessRuntime;
|
|
|
|
use async_trait::async_trait;
|
|
use attune_common::models::runtime::RuntimeExecutionConfig;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
use thiserror::Error;
|
|
use tokio_util::sync::CancellationToken;
|
|
|
|
// Re-export dependency management types
|
|
pub use dependency::{
|
|
DependencyError, DependencyManager, DependencyManagerRegistry, DependencyResult,
|
|
DependencySpec, EnvironmentInfo,
|
|
};
|
|
pub use log_writer::{BoundedLogResult, BoundedLogWriter};
|
|
pub use parameter_passing::{ParameterDeliveryConfig, PreparedParameters};
|
|
|
|
// Re-export parameter types from common
|
|
pub use attune_common::models::{OutputFormat, ParameterDelivery, ParameterFormat};
|
|
|
|
/// Runtime execution result
|
|
pub type RuntimeResult<T> = std::result::Result<T, RuntimeError>;
|
|
|
|
/// Runtime execution errors
|
|
#[derive(Debug, Error)]
|
|
pub enum RuntimeError {
|
|
#[error("Execution failed: {0}")]
|
|
ExecutionFailed(String),
|
|
|
|
#[error("Timeout after {0} seconds")]
|
|
Timeout(u64),
|
|
|
|
#[error("Runtime not found: {0}")]
|
|
RuntimeNotFound(String),
|
|
|
|
#[error("Invalid action: {0}")]
|
|
InvalidAction(String),
|
|
|
|
#[error("IO error: {0}")]
|
|
IoError(#[from] std::io::Error),
|
|
|
|
#[error("Serialization error: {0}")]
|
|
SerializationError(#[from] serde_json::Error),
|
|
|
|
#[error("Process error: {0}")]
|
|
ProcessError(String),
|
|
|
|
#[error("Setup error: {0}")]
|
|
SetupError(String),
|
|
|
|
#[error("Cleanup error: {0}")]
|
|
CleanupError(String),
|
|
}
|
|
|
|
/// Action execution context
|
|
#[derive(Debug, Clone)]
|
|
pub struct ExecutionContext {
|
|
/// Execution ID
|
|
pub execution_id: i64,
|
|
|
|
/// Action reference (pack.action)
|
|
pub action_ref: String,
|
|
|
|
/// Action parameters
|
|
pub parameters: HashMap<String, serde_json::Value>,
|
|
|
|
/// Environment variables
|
|
pub env: HashMap<String, String>,
|
|
|
|
/// Secrets (passed securely via stdin, not environment variables).
|
|
/// Values are JSON — strings, objects, arrays, numbers, or booleans.
|
|
pub secrets: HashMap<String, serde_json::Value>,
|
|
|
|
/// Execution timeout in seconds
|
|
pub timeout: Option<u64>,
|
|
|
|
/// Working directory
|
|
pub working_dir: Option<PathBuf>,
|
|
|
|
/// Action entry point (script, function, etc.)
|
|
pub entry_point: String,
|
|
|
|
/// Action code/script content
|
|
pub code: Option<String>,
|
|
|
|
/// Action code file path (alternative to code)
|
|
pub code_path: Option<PathBuf>,
|
|
|
|
/// Runtime name (python, shell, etc.) - used to select the correct runtime
|
|
pub runtime_name: Option<String>,
|
|
|
|
/// Optional override of the runtime's execution config, set when a specific
|
|
/// runtime version has been selected (e.g., Python 3.12 vs the parent
|
|
/// "Python" runtime). When present, `ProcessRuntime` uses this config
|
|
/// instead of its built-in one for interpreter resolution, environment
|
|
/// setup, and dependency management.
|
|
pub runtime_config_override: Option<RuntimeExecutionConfig>,
|
|
|
|
/// Optional override of the environment directory suffix. When a specific
|
|
/// runtime version is selected, the env dir includes the version
|
|
/// (e.g., `python-3.12` instead of `python`) for per-version isolation.
|
|
/// Format: just the directory name, not the full path.
|
|
pub runtime_env_dir_suffix: Option<String>,
|
|
|
|
/// The selected runtime version string for logging/diagnostics
|
|
/// (e.g., "3.12.1"). `None` means the parent runtime config is used as-is.
|
|
pub selected_runtime_version: Option<String>,
|
|
|
|
/// Maximum stdout size in bytes (for log truncation)
|
|
pub max_stdout_bytes: usize,
|
|
|
|
/// Maximum stderr size in bytes (for log truncation)
|
|
pub max_stderr_bytes: usize,
|
|
|
|
/// How parameters should be delivered to the action
|
|
pub parameter_delivery: ParameterDelivery,
|
|
|
|
/// Format for parameter serialization
|
|
pub parameter_format: ParameterFormat,
|
|
|
|
/// Format for output parsing
|
|
pub output_format: OutputFormat,
|
|
|
|
/// Optional cancellation token for process termination.
|
|
/// When triggered, the executor sends SIGTERM → SIGKILL
|
|
/// with a short grace period.
|
|
pub cancel_token: Option<CancellationToken>,
|
|
}
|
|
|
|
impl ExecutionContext {
|
|
/// Create a test context with default values (for tests)
|
|
#[cfg(test)]
|
|
pub fn test_context(action_ref: String, code: Option<String>) -> Self {
|
|
use std::collections::HashMap;
|
|
Self {
|
|
execution_id: 1,
|
|
action_ref,
|
|
parameters: HashMap::new(),
|
|
env: HashMap::new(),
|
|
secrets: HashMap::new(),
|
|
timeout: Some(10),
|
|
working_dir: None,
|
|
entry_point: "run".to_string(),
|
|
code,
|
|
code_path: None,
|
|
runtime_name: None,
|
|
runtime_config_override: None,
|
|
runtime_env_dir_suffix: None,
|
|
selected_runtime_version: None,
|
|
max_stdout_bytes: 10 * 1024 * 1024,
|
|
max_stderr_bytes: 10 * 1024 * 1024,
|
|
parameter_delivery: ParameterDelivery::default(),
|
|
parameter_format: ParameterFormat::default(),
|
|
output_format: OutputFormat::default(),
|
|
cancel_token: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Action execution result
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ExecutionResult {
|
|
/// Exit code (0 = success)
|
|
pub exit_code: i32,
|
|
|
|
/// Standard output
|
|
pub stdout: String,
|
|
|
|
/// Standard error
|
|
pub stderr: String,
|
|
|
|
/// Execution result data (parsed from stdout or returned by action)
|
|
pub result: Option<serde_json::Value>,
|
|
|
|
/// Execution duration in milliseconds
|
|
pub duration_ms: u64,
|
|
|
|
/// Error message if execution failed
|
|
pub error: Option<String>,
|
|
|
|
/// Whether stdout was truncated due to size limits
|
|
#[serde(default)]
|
|
pub stdout_truncated: bool,
|
|
|
|
/// Whether stderr was truncated due to size limits
|
|
#[serde(default)]
|
|
pub stderr_truncated: bool,
|
|
|
|
/// Number of bytes truncated from stdout (0 if not truncated)
|
|
#[serde(default)]
|
|
pub stdout_bytes_truncated: usize,
|
|
|
|
/// Number of bytes truncated from stderr (0 if not truncated)
|
|
#[serde(default)]
|
|
pub stderr_bytes_truncated: usize,
|
|
}
|
|
|
|
impl ExecutionResult {
|
|
/// Check if execution was successful
|
|
pub fn is_success(&self) -> bool {
|
|
self.exit_code == 0 && self.error.is_none()
|
|
}
|
|
|
|
/// Create a success result
|
|
pub fn success(stdout: String, result: Option<serde_json::Value>, duration_ms: u64) -> Self {
|
|
Self {
|
|
exit_code: 0,
|
|
stdout,
|
|
stderr: String::new(),
|
|
result,
|
|
duration_ms,
|
|
error: None,
|
|
stdout_truncated: false,
|
|
stderr_truncated: false,
|
|
stdout_bytes_truncated: 0,
|
|
stderr_bytes_truncated: 0,
|
|
}
|
|
}
|
|
|
|
/// Create a failure result
|
|
pub fn failure(exit_code: i32, stderr: String, error: String, duration_ms: u64) -> Self {
|
|
Self {
|
|
exit_code,
|
|
stdout: String::new(),
|
|
stderr,
|
|
result: None,
|
|
duration_ms,
|
|
error: Some(error),
|
|
stdout_truncated: false,
|
|
stderr_truncated: false,
|
|
stdout_bytes_truncated: 0,
|
|
stderr_bytes_truncated: 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Runtime trait for executing actions
|
|
#[async_trait]
|
|
pub trait Runtime: Send + Sync {
|
|
/// Get the runtime name
|
|
fn name(&self) -> &str;
|
|
|
|
/// Check if this runtime can execute the given action
|
|
fn can_execute(&self, context: &ExecutionContext) -> bool;
|
|
|
|
/// Execute an action
|
|
async fn execute(&self, context: ExecutionContext) -> RuntimeResult<ExecutionResult>;
|
|
|
|
/// Setup the runtime environment (called once on worker startup)
|
|
async fn setup(&self) -> RuntimeResult<()> {
|
|
Ok(())
|
|
}
|
|
|
|
/// Cleanup the runtime environment (called on worker shutdown)
|
|
async fn cleanup(&self) -> RuntimeResult<()> {
|
|
Ok(())
|
|
}
|
|
|
|
/// Validate the runtime is properly configured
|
|
async fn validate(&self) -> RuntimeResult<()> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Runtime registry for managing multiple runtime implementations
|
|
pub struct RuntimeRegistry {
|
|
runtimes: Vec<Box<dyn Runtime>>,
|
|
}
|
|
|
|
impl RuntimeRegistry {
|
|
/// Create a new runtime registry
|
|
pub fn new() -> Self {
|
|
Self {
|
|
runtimes: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Register a runtime
|
|
pub fn register(&mut self, runtime: Box<dyn Runtime>) {
|
|
self.runtimes.push(runtime);
|
|
}
|
|
|
|
/// Get a runtime that can execute the given context
|
|
pub fn get_runtime(&self, context: &ExecutionContext) -> RuntimeResult<&dyn Runtime> {
|
|
// If runtime_name is specified, use it to select the runtime directly
|
|
if let Some(ref runtime_name) = context.runtime_name {
|
|
return self
|
|
.runtimes
|
|
.iter()
|
|
.find(|r| r.name() == runtime_name)
|
|
.map(|r| r.as_ref())
|
|
.ok_or_else(|| {
|
|
RuntimeError::RuntimeNotFound(format!(
|
|
"Runtime '{}' not found for action: {} (available: {})",
|
|
runtime_name,
|
|
context.action_ref,
|
|
self.list_runtimes().join(", ")
|
|
))
|
|
});
|
|
}
|
|
|
|
// Otherwise, fall back to can_execute check
|
|
self.runtimes
|
|
.iter()
|
|
.find(|r| r.can_execute(context))
|
|
.map(|r| r.as_ref())
|
|
.ok_or_else(|| {
|
|
RuntimeError::RuntimeNotFound(format!(
|
|
"No runtime found for action: {} (available: {})",
|
|
context.action_ref,
|
|
self.list_runtimes().join(", ")
|
|
))
|
|
})
|
|
}
|
|
|
|
/// Setup all registered runtimes
|
|
pub async fn setup_all(&self) -> RuntimeResult<()> {
|
|
for runtime in &self.runtimes {
|
|
runtime.setup().await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Cleanup all registered runtimes
|
|
pub async fn cleanup_all(&self) -> RuntimeResult<()> {
|
|
for runtime in &self.runtimes {
|
|
runtime.cleanup().await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Validate all registered runtimes
|
|
pub async fn validate_all(&self) -> RuntimeResult<()> {
|
|
for runtime in &self.runtimes {
|
|
runtime.validate().await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// List all registered runtimes
|
|
pub fn list_runtimes(&self) -> Vec<&str> {
|
|
self.runtimes.iter().map(|r| r.name()).collect()
|
|
}
|
|
}
|
|
|
|
impl Default for RuntimeRegistry {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|