mecha10-runtime 0.9.0

Runtime supervisor for Mecha10 nodes - launching, monitoring, and lifecycle management
Documentation
//! Launcher service integration for dynamic node launching
//!
//! LAB-2212 investigated deleting this module as part of consolidating node-lifecycle runners
//! onto `mecha10-node-runner`, on the premise that only this module's own tests referenced it.
//! That premise doesn't hold: `Launcher` is wired into `crate::runtime::Runtime`/`RuntimeBuilder`
//! as an optional feature (`RuntimeBuilder::with_launcher`, `Runtime::launcher()`, both public
//! API), not merely exercised by its own unit tests - `packages/runtime/tests/launcher_tests.rs`
//! is a separate integration-test crate driving that same `Runtime`-level surface. No workspace
//! package outside `runtime` currently calls `with_launcher`/`Runtime::launcher()`, so the
//! *feature* is unused end-to-end today, but the module itself is genuinely reachable/compiled
//! crate-internal code, not orphaned dead code - so it was kept as-is rather than deleted. If this
//! `Runtime`-level launcher feature is still unwanted, retiring it needs a follow-up that also
//! removes the `with_launcher`/`launcher()` surface from `runtime.rs` and the integration test
//! suite, not just this file.

use crate::node::NodeRunner;
use crate::supervisor::{NodeHandle, Supervisor};
use anyhow::Result;
use mecha10_core::rwlock_collections::RwLockMapExt;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;

pub type NodeFactory = Box<dyn Fn() -> Box<dyn NodeRunner> + Send + Sync>;

pub struct Launcher {
    supervisor: Arc<Supervisor>,
    factories: Arc<RwLock<HashMap<String, NodeFactory>>>,
    running_nodes: Arc<RwLock<HashMap<String, NodeHandle>>>,
}

impl Launcher {
    /// Create a new launcher
    pub fn new(supervisor: Arc<Supervisor>) -> Self {
        Self {
            supervisor,
            factories: Arc::new(RwLock::new(HashMap::new())),
            running_nodes: Arc::new(RwLock::new(HashMap::new())),
        }
    }

    /// Register a node type with a factory function
    pub async fn register<F>(&self, node_type: String, factory: F)
    where
        F: Fn() -> Box<dyn NodeRunner> + Send + Sync + 'static,
    {
        let mut factories = self.factories.write().await;
        factories.insert(node_type, Box::new(factory));
    }

    /// Launch a node by type name
    pub async fn launch(&self, node_type: &str) -> Result<String> {
        let factories = self.factories.read().await;
        let factory = factories
            .get(node_type)
            .ok_or_else(|| anyhow::anyhow!("Unknown node type: {}", node_type))?;

        let node = factory();
        let node_name = node.name().to_string();

        let running = self.running_nodes.read().await;
        if running.contains_key(&node_name) {
            anyhow::bail!("Node already running: {}", node_name);
        }
        drop(running);

        let handle = self.supervisor.launch_node(node);
        let name = handle.name().to_string();

        let mut running = self.running_nodes.write().await;
        running.insert(name.clone(), handle);

        Ok(name)
    }

    /// Stop a running node
    pub async fn stop(&self, node_name: &str) -> Result<()> {
        let mut running = self.running_nodes.write().await;
        let handle = running
            .remove(node_name)
            .ok_or_else(|| anyhow::anyhow!("Node not running: {}", node_name))?;

        handle.abort();
        Ok(())
    }

    /// List all running nodes
    pub async fn list_running(&self) -> Vec<String> {
        let running = self.running_nodes.read().await;
        running.keys().cloned().collect()
    }

    /// Check if a node is running
    pub async fn is_running(&self, node_name: &str) -> bool {
        let running = self.running_nodes.read().await;
        running.contains_key(node_name)
    }

    /// Get list of registered node types
    pub async fn list_types(&self) -> Vec<String> {
        self.factories.cloned_keys().await
    }
}