2026-02-16 05:09:51 +00:00

309 lines
9.7 KiB
Rust

// Sandbox Dispatcher - Auto-tier selection
// Routes commands to WASM (fast) or Docker (compatible) based on complexity
use std::sync::Arc;
use std::time::Instant;
use tracing::{debug, info};
use crate::sandbox::docker_pool::DockerPool;
use crate::sandbox::wasm_runtime::WasmRuntime;
use crate::sandbox::{SandboxCommand, SandboxResult};
use crate::RLMError;
/// Sandbox tier selection
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SandboxTier {
/// Tier 1: WASM runtime (fast, <5ms target)
Wasm,
/// Tier 2: Docker pool (compatible, 80-150ms target)
Docker,
}
impl std::fmt::Display for SandboxTier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SandboxTier::Wasm => write!(f, "wasm"),
SandboxTier::Docker => write!(f, "docker"),
}
}
}
/// Dispatcher configuration
#[derive(Debug, Clone)]
pub struct DispatcherConfig {
/// Enable WASM tier
pub enable_wasm: bool,
/// Enable Docker tier
pub enable_docker: bool,
/// Fallback to Docker if WASM fails
pub fallback_to_docker: bool,
}
impl Default for DispatcherConfig {
fn default() -> Self {
Self {
enable_wasm: true,
enable_docker: true,
fallback_to_docker: true,
}
}
}
/// Sandbox dispatcher - routes commands to appropriate tier
pub struct SandboxDispatcher {
wasm_runtime: Option<Arc<WasmRuntime>>,
docker_pool: Option<Arc<DockerPool>>,
config: DispatcherConfig,
}
impl SandboxDispatcher {
/// Create a new dispatcher with both tiers
pub async fn new(
wasm_runtime: Option<Arc<WasmRuntime>>,
docker_pool: Option<Arc<DockerPool>>,
) -> crate::Result<Self> {
Ok(Self {
wasm_runtime,
docker_pool,
config: DispatcherConfig::default(),
})
}
/// Create with custom configuration
pub async fn with_config(
wasm_runtime: Option<Arc<WasmRuntime>>,
docker_pool: Option<Arc<DockerPool>>,
config: DispatcherConfig,
) -> crate::Result<Self> {
Ok(Self {
wasm_runtime,
docker_pool,
config,
})
}
/// Execute a command, automatically selecting the appropriate tier
///
/// # Tier Selection Logic
/// 1. WASI-compatible commands → Tier 1 (WASM) if enabled
/// 2. Complex commands → Tier 2 (Docker) if enabled
/// 3. Fallback: Docker if WASM fails and fallback enabled
///
/// # Returns
/// SandboxResult with tier information
pub async fn execute(&self, command: &SandboxCommand) -> crate::Result<SandboxResult> {
let start = Instant::now();
// Select tier
let tier = self.select_tier(&command.command);
debug!(
"Dispatching command '{}' to {:?} tier",
command.command, tier
);
// Execute in selected tier
let result = match tier {
SandboxTier::Wasm => {
if let Some(ref wasm_runtime) = self.wasm_runtime {
match wasm_runtime.execute(command) {
Ok(result) => Ok(result),
Err(e) if self.config.fallback_to_docker => {
info!("WASM execution failed, falling back to Docker: {}", e);
self.execute_docker(command).await
}
Err(e) => Err(e),
}
} else {
return Err(RLMError::SandboxError(
"WASM tier not available".to_string(),
));
}
}
SandboxTier::Docker => self.execute_docker(command).await,
}?;
let duration = start.elapsed();
debug!(
"Dispatched command '{}' via {} in {:?}",
command.command, result.tier, duration
);
Ok(result)
}
/// Execute in Docker tier
async fn execute_docker(&self, command: &SandboxCommand) -> crate::Result<SandboxResult> {
if let Some(ref docker_pool) = self.docker_pool {
docker_pool.execute(command).await
} else {
Err(RLMError::SandboxError(
"Docker tier not available".to_string(),
))
}
}
/// Select tier based on command complexity
fn select_tier(&self, command: &str) -> SandboxTier {
// WASI-compatible commands go to WASM tier (if enabled and available)
if self.config.enable_wasm
&& self.wasm_runtime.is_some()
&& self.is_wasi_compatible(command)
{
return SandboxTier::Wasm;
}
// Non-WASI commands prefer Docker (if enabled AND available)
if self.config.enable_docker && self.docker_pool.is_some() {
return SandboxTier::Docker;
}
// Fallback: If Docker enabled but unavailable, use WASM if available
if self.config.enable_docker && self.docker_pool.is_none() && self.wasm_runtime.is_some() {
return SandboxTier::Wasm;
}
// If WASM enabled and available (for non-WASI commands when Docker not
// preferred)
if self.config.enable_wasm && self.wasm_runtime.is_some() {
return SandboxTier::Wasm;
}
// Last resort: Docker (will error on execute if not available)
SandboxTier::Docker
}
/// Check if command is WASI-compatible
fn is_wasi_compatible(&self, command: &str) -> bool {
matches!(command, "peek" | "grep" | "slice")
}
/// Get tier usage statistics
pub fn tier_stats(&self) -> TierStats {
// In a real implementation, would track tier usage in metrics
// For Phase 4, return basic info
TierStats {
wasm_available: self.wasm_runtime.is_some(),
docker_available: self.docker_pool.is_some(),
docker_pool_size: self
.docker_pool
.as_ref()
.map(|p| p.pool_size())
.unwrap_or(0),
}
}
}
/// Tier usage statistics
#[derive(Debug, Clone)]
pub struct TierStats {
pub wasm_available: bool,
pub docker_available: bool,
pub docker_pool_size: usize,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sandbox::wasm_runtime::WasmRuntime;
#[tokio::test]
async fn test_dispatcher_creation() {
let wasm = Some(Arc::new(WasmRuntime::new()));
let dispatcher = SandboxDispatcher::new(wasm, None).await.unwrap();
let stats = dispatcher.tier_stats();
assert!(stats.wasm_available);
assert!(!stats.docker_available);
}
#[tokio::test]
async fn test_tier_selection_wasi_compatible() {
let wasm = Some(Arc::new(WasmRuntime::new()));
let dispatcher = SandboxDispatcher::new(wasm, None).await.unwrap();
assert_eq!(dispatcher.select_tier("peek"), SandboxTier::Wasm);
assert_eq!(dispatcher.select_tier("grep"), SandboxTier::Wasm);
assert_eq!(dispatcher.select_tier("slice"), SandboxTier::Wasm);
}
#[tokio::test]
async fn test_tier_selection_complex_command() {
let wasm = Some(Arc::new(WasmRuntime::new()));
let dispatcher = SandboxDispatcher::new(wasm, None).await.unwrap();
// Complex commands should prefer Docker (but WASM is selected as fallback if
// Docker unavailable)
assert_eq!(dispatcher.select_tier("bash"), SandboxTier::Wasm); // Fallback
}
#[tokio::test]
async fn test_execute_wasm_tier() {
let wasm = Some(Arc::new(WasmRuntime::new()));
let dispatcher = SandboxDispatcher::new(wasm, None).await.unwrap();
let command = SandboxCommand::new("peek")
.arg("3")
.stdin("line1\nline2\nline3\nline4");
let result = dispatcher.execute(&command).await.unwrap();
assert!(result.is_success());
assert_eq!(result.tier, SandboxTier::Wasm);
assert_eq!(result.output, "line1\nline2\nline3\n");
}
#[tokio::test]
async fn test_execute_grep_wasm() {
let wasm = Some(Arc::new(WasmRuntime::new()));
let dispatcher = SandboxDispatcher::new(wasm, None).await.unwrap();
let command = SandboxCommand::new("grep")
.arg("error")
.stdin("info: ok\nerror: failed\nwarn: retry");
let result = dispatcher.execute(&command).await.unwrap();
assert!(result.is_success());
assert_eq!(result.tier, SandboxTier::Wasm);
assert!(result.output.contains("error: failed"));
}
#[tokio::test]
async fn test_wasm_not_available() {
let dispatcher = SandboxDispatcher::new(None, None).await.unwrap();
let command = SandboxCommand::new("peek").arg("5").stdin("test");
let result = dispatcher.execute(&command).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_custom_config() {
let config = DispatcherConfig {
enable_wasm: false,
enable_docker: false,
fallback_to_docker: false,
};
let wasm = Some(Arc::new(WasmRuntime::new()));
let dispatcher = SandboxDispatcher::with_config(wasm, None, config)
.await
.unwrap();
// With WASM disabled, should select Docker (even though unavailable)
assert_eq!(dispatcher.select_tier("peek"), SandboxTier::Docker);
}
#[tokio::test]
async fn test_tier_stats() {
let wasm = Some(Arc::new(WasmRuntime::new()));
let dispatcher = SandboxDispatcher::new(wasm, None).await.unwrap();
let stats = dispatcher.tier_stats();
assert!(stats.wasm_available);
assert!(!stats.docker_available);
assert_eq!(stats.docker_pool_size, 0);
}
}