Skip to main content

modkit/backends/
mod.rs

1//! Backend abstraction for out-of-process module management
2//!
3//! This module provides traits and types for spawning and managing `OoP` module instances.
4
5use anyhow::Result;
6use async_trait::async_trait;
7use std::collections::HashMap;
8use std::path::PathBuf;
9use std::time::Instant;
10use uuid::Uuid;
11
12/// The kind of backend used to spawn and manage module instances
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum BackendKind {
15    LocalProcess,
16    K8s,
17    Static,
18    Mock,
19}
20
21/// Configuration for an out-of-process module
22pub struct OopModuleConfig {
23    pub name: String,
24    pub binary: Option<PathBuf>,
25    pub args: Vec<String>,
26    pub env: HashMap<String, String>,
27    pub working_directory: Option<String>,
28    pub backend: BackendKind,
29    pub version: Option<String>,
30}
31
32impl OopModuleConfig {
33    pub fn new(name: impl Into<String>, backend: BackendKind) -> Self {
34        Self {
35            name: name.into(),
36            binary: None,
37            args: Vec::new(),
38            env: HashMap::new(),
39            working_directory: None,
40            backend,
41            version: None,
42        }
43    }
44}
45
46/// A handle to a running module instance
47#[derive(Clone)]
48pub struct InstanceHandle {
49    pub module: String,
50    pub instance_id: Uuid,
51    pub backend: BackendKind,
52    pub pid: Option<u32>,
53    pub created_at: Instant,
54}
55
56impl std::fmt::Debug for InstanceHandle {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        f.debug_struct("InstanceHandle")
59            .field("module", &self.module)
60            .field("instance_id", &self.instance_id)
61            .field("backend", &self.backend)
62            .field("pid", &self.pid)
63            .field("created_at", &self.created_at)
64            .finish()
65    }
66}
67
68/// Trait for backends that can spawn and manage module instances
69#[async_trait]
70pub trait ModuleRuntimeBackend: Send + Sync {
71    async fn spawn_instance(&self, cfg: &OopModuleConfig) -> Result<InstanceHandle>;
72    async fn stop_instance(&self, handle: &InstanceHandle) -> Result<()>;
73    async fn list_instances(&self, module: &str) -> Result<Vec<InstanceHandle>>;
74}
75
76/// Configuration passed to `OopBackend::spawn`
77pub struct OopSpawnConfig {
78    pub module_name: String,
79    pub binary: PathBuf,
80    pub args: Vec<String>,
81    pub env: HashMap<String, String>,
82    pub working_directory: Option<String>,
83}
84
85/// A type-erased backend for spawning `OoP` modules.
86///
87/// This trait is used by `HostRuntime` to spawn `OoP` modules after the start phase.
88#[async_trait]
89pub trait OopBackend: Send + Sync {
90    /// Spawn an `OoP` module instance.
91    async fn spawn(&self, config: OopSpawnConfig) -> Result<()>;
92
93    /// Shutdown all spawned instances (called during stop phase).
94    async fn shutdown_all(&self);
95}
96
97pub mod local;
98pub mod log_forwarder;
99
100pub use local::LocalProcessBackend;
101
102/// Adapter that implements `OopBackend` trait for `LocalProcessBackend`.
103///
104/// This allows `LocalProcessBackend` to be used by `HostRuntime` for spawning `OoP` modules.
105#[async_trait]
106impl OopBackend for LocalProcessBackend {
107    async fn spawn(&self, config: OopSpawnConfig) -> Result<()> {
108        let mut oop_config = OopModuleConfig::new(&config.module_name, BackendKind::LocalProcess);
109        oop_config.binary = Some(config.binary);
110        oop_config.args = config.args;
111        oop_config.env = config.env;
112        oop_config.working_directory = config.working_directory;
113
114        self.spawn_instance(&oop_config).await?;
115        Ok(())
116    }
117
118    async fn shutdown_all(&self) {
119        // The LocalProcessBackend already handles shutdown via its cancellation token
120        // when the token is triggered, it automatically stops all instances.
121        // This method is a no-op because the backend's internal shutdown task handles it.
122    }
123}
124
125#[cfg(test)]
126#[cfg_attr(coverage_nightly, coverage(off))]
127mod tests {
128    use super::*;
129    use std::path::PathBuf;
130
131    #[test]
132    fn test_oop_module_config_builder() {
133        let mut cfg = OopModuleConfig::new("my_module", BackendKind::LocalProcess);
134        cfg.binary = Some(PathBuf::from("/usr/bin/myapp"));
135        cfg.args = vec!["--port".to_owned(), "8080".to_owned()];
136        cfg.env.insert("LOG_LEVEL".to_owned(), "debug".to_owned());
137        cfg.version = Some("1.0.0".to_owned());
138
139        assert_eq!(cfg.name, "my_module");
140        assert_eq!(cfg.backend, BackendKind::LocalProcess);
141        assert_eq!(cfg.binary, Some(PathBuf::from("/usr/bin/myapp")));
142        assert_eq!(cfg.args.len(), 2);
143        assert_eq!(cfg.env.len(), 1);
144        assert_eq!(cfg.version, Some("1.0.0".to_owned()));
145    }
146
147    #[test]
148    fn test_backend_kind_equality() {
149        assert_eq!(BackendKind::LocalProcess, BackendKind::LocalProcess);
150        assert_ne!(BackendKind::LocalProcess, BackendKind::K8s);
151        assert_ne!(BackendKind::K8s, BackendKind::Static);
152        assert_ne!(BackendKind::Static, BackendKind::Mock);
153    }
154
155    #[test]
156    fn test_instance_handle_debug() {
157        let instance_id = Uuid::new_v4();
158        let handle = InstanceHandle {
159            module: "test_module".to_owned(),
160            instance_id,
161            backend: BackendKind::LocalProcess,
162            pid: Some(12345),
163            created_at: Instant::now(),
164        };
165
166        let debug_str = format!("{handle:?}");
167        assert!(debug_str.contains("test_module"));
168        assert!(debug_str.contains(&instance_id.to_string()));
169        assert!(debug_str.contains("LocalProcess"));
170        assert!(debug_str.contains("12345"));
171    }
172}