use anyhow::Result;
use async_trait::async_trait;
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Instant;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackendKind {
LocalProcess,
K8s,
Static,
Mock,
}
pub struct OopModuleConfig {
pub name: String,
pub binary: Option<PathBuf>,
pub args: Vec<String>,
pub env: HashMap<String, String>,
pub working_directory: Option<String>,
pub backend: BackendKind,
pub version: Option<String>,
}
impl OopModuleConfig {
pub fn new(name: impl Into<String>, backend: BackendKind) -> Self {
Self {
name: name.into(),
binary: None,
args: Vec::new(),
env: HashMap::new(),
working_directory: None,
backend,
version: None,
}
}
}
#[derive(Clone)]
pub struct InstanceHandle {
pub module: String,
pub instance_id: Uuid,
pub backend: BackendKind,
pub pid: Option<u32>,
pub created_at: Instant,
}
impl std::fmt::Debug for InstanceHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InstanceHandle")
.field("module", &self.module)
.field("instance_id", &self.instance_id)
.field("backend", &self.backend)
.field("pid", &self.pid)
.field("created_at", &self.created_at)
.finish()
}
}
#[async_trait]
pub trait ModuleRuntimeBackend: Send + Sync {
async fn spawn_instance(&self, cfg: &OopModuleConfig) -> Result<InstanceHandle>;
async fn stop_instance(&self, handle: &InstanceHandle) -> Result<()>;
async fn list_instances(&self, module: &str) -> Result<Vec<InstanceHandle>>;
}
pub struct OopSpawnConfig {
pub module_name: String,
pub binary: PathBuf,
pub args: Vec<String>,
pub env: HashMap<String, String>,
pub working_directory: Option<String>,
}
#[async_trait]
pub trait OopBackend: Send + Sync {
async fn spawn(&self, config: OopSpawnConfig) -> Result<()>;
async fn shutdown_all(&self);
}
pub mod local;
pub mod log_forwarder;
pub use local::LocalProcessBackend;
#[async_trait]
impl OopBackend for LocalProcessBackend {
async fn spawn(&self, config: OopSpawnConfig) -> Result<()> {
let mut oop_config = OopModuleConfig::new(&config.module_name, BackendKind::LocalProcess);
oop_config.binary = Some(config.binary);
oop_config.args = config.args;
oop_config.env = config.env;
oop_config.working_directory = config.working_directory;
self.spawn_instance(&oop_config).await?;
Ok(())
}
async fn shutdown_all(&self) {
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_oop_module_config_builder() {
let mut cfg = OopModuleConfig::new("my_module", BackendKind::LocalProcess);
cfg.binary = Some(PathBuf::from("/usr/bin/myapp"));
cfg.args = vec!["--port".to_owned(), "8080".to_owned()];
cfg.env.insert("LOG_LEVEL".to_owned(), "debug".to_owned());
cfg.version = Some("1.0.0".to_owned());
assert_eq!(cfg.name, "my_module");
assert_eq!(cfg.backend, BackendKind::LocalProcess);
assert_eq!(cfg.binary, Some(PathBuf::from("/usr/bin/myapp")));
assert_eq!(cfg.args.len(), 2);
assert_eq!(cfg.env.len(), 1);
assert_eq!(cfg.version, Some("1.0.0".to_owned()));
}
#[test]
fn test_backend_kind_equality() {
assert_eq!(BackendKind::LocalProcess, BackendKind::LocalProcess);
assert_ne!(BackendKind::LocalProcess, BackendKind::K8s);
assert_ne!(BackendKind::K8s, BackendKind::Static);
assert_ne!(BackendKind::Static, BackendKind::Mock);
}
#[test]
fn test_instance_handle_debug() {
let instance_id = Uuid::new_v4();
let handle = InstanceHandle {
module: "test_module".to_owned(),
instance_id,
backend: BackendKind::LocalProcess,
pid: Some(12345),
created_at: Instant::now(),
};
let debug_str = format!("{handle:?}");
assert!(debug_str.contains("test_module"));
assert!(debug_str.contains(&instance_id.to_string()));
assert!(debug_str.contains("LocalProcess"));
assert!(debug_str.contains("12345"));
}
}