mecha10-runtime 0.8.0

Runtime supervisor for Mecha10 nodes - launching, monitoring, and lifecycle management
Documentation
//! Lifecycle management for mode-based dynamic node control
//!
//! This module provides the lifecycle manager that orchestrates node
//! start/stop based on operational mode changes.
//!
//! # Architecture
//!
//! ```text
//! CLI → Redis (mode/request) → LifecycleManager → Supervisor → Nodes
//! ```
//!
//! The lifecycle manager:
//! - Subscribes to mode change requests
//! - Calculates which nodes need to start/stop
//! - Uses Supervisor to execute changes
//! - Publishes mode changed events
//!
//! # Example
//!
//! ```rust,ignore
//! use mecha10_runtime::lifecycle::LifecycleManager;
//!
//! let lifecycle = LifecycleManager::new(
//!     context,
//!     supervisor,
//!     mode_config,
//!     "startup".to_string(),
//! );
//!
//! // Run in background
//! tokio::spawn(async move {
//!     lifecycle.run().await
//! });
//! ```

use anyhow::Result;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use tracing::{debug, error, info, warn};

pub struct LifecycleManager {
    current_mode: String,

    pub(crate) running_nodes: HashSet<String>,

    mode_config: HashMap<String, ModeConfig>,

    supervisor: Arc<dyn SupervisorTrait>,
}

#[derive(Debug, Clone)]
pub struct ModeConfig {
    pub nodes: Vec<String>,

    pub description: Option<String>,
}

#[async_trait::async_trait]
pub trait SupervisorTrait: Send + Sync {
    /// Start a node by name
    async fn start_node(&self, name: &str) -> Result<()>;

    /// Stop a node by name
    async fn stop_node(&self, name: &str) -> Result<()>;

    /// Get list of running nodes
    fn get_running_nodes(&self) -> Vec<String>;
}

impl LifecycleManager {
    /// Create a new lifecycle manager
    ///
    /// # Arguments
    ///
    /// * `supervisor` - Supervisor instance for node management
    /// * `mode_config` - Map of mode name to mode configuration
    /// * `default_mode` - Initial mode to start in
    pub fn new(
        supervisor: Arc<dyn SupervisorTrait>,
        mode_config: HashMap<String, ModeConfig>,
        default_mode: String,
    ) -> Self {
        Self {
            current_mode: default_mode,
            running_nodes: HashSet::new(),
            mode_config,
            supervisor,
        }
    }

    /// Get the current mode
    pub fn current_mode(&self) -> &str {
        &self.current_mode
    }

    /// Get running nodes
    pub fn running_nodes(&self) -> &HashSet<String> {
        &self.running_nodes
    }

    /// Request a mode change
    ///
    /// This is the main entry point for mode transitions.
    /// It calculates the diff and executes the transition.
    ///
    /// # Arguments
    ///
    /// * `target_mode` - The mode to transition to
    ///
    /// # Returns
    ///
    /// Ok with nodes started/stopped, or Err if mode doesn't exist
    pub async fn change_mode(&mut self, target_mode: &str) -> Result<ModeTransitionResult> {
        info!("Mode change requested: {} -> {}", self.current_mode, target_mode);

        if !self.mode_config.contains_key(target_mode) {
            let available: Vec<_> = self.mode_config.keys().map(|k| k.as_str()).collect();
            return Err(anyhow::anyhow!(
                "Mode '{}' not found. Available modes: {}",
                target_mode,
                available.join(", ")
            ));
        }

        let diff = self.calculate_mode_diff(target_mode)?;

        info!("Mode transition plan: stop {:?}, start {:?}", diff.stop, diff.start);

        self.execute_mode_transition(target_mode, diff).await
    }

    /// Calculate which nodes need to start/stop for a mode transition
    pub fn calculate_mode_diff(&self, target_mode: &str) -> Result<ModeDiff> {
        let target_config = self
            .mode_config
            .get(target_mode)
            .ok_or_else(|| anyhow::anyhow!("Mode not found"))?;

        let (start, stop) = diff_nodes(&target_config.nodes, &self.running_nodes);

        Ok(ModeDiff { start, stop })
    }

    /// Execute a mode transition
    async fn execute_mode_transition(&mut self, target_mode: &str, diff: ModeDiff) -> Result<ModeTransitionResult> {
        debug!(
            "Executing mode transition: {} -> {} (stop: {:?}, start: {:?})",
            self.current_mode, target_mode, diff.stop, diff.start
        );

        let mut stopped = Vec::new();
        let mut started = Vec::new();
        let mut errors = Vec::new();

        for node in &diff.stop {
            info!("Stopping node: {}", node);
            match self.supervisor.stop_node(node).await {
                Ok(()) => {
                    self.running_nodes.remove(node);
                    stopped.push(node.clone());
                    debug!("✓ Stopped: {}", node);
                }
                Err(e) => {
                    warn!("Failed to stop node {}: {}", node, e);
                    errors.push(format!("stop {}: {}", node, e));
                }
            }
        }

        for node in &diff.start {
            info!("Starting node: {}", node);
            match self.supervisor.start_node(node).await {
                Ok(()) => {
                    self.running_nodes.insert(node.clone());
                    started.push(node.clone());
                    debug!("✓ Started: {}", node);
                }
                Err(e) => {
                    error!("Failed to start node {}: {}", node, e);
                    errors.push(format!("start {}: {}", node, e));
                }
            }
        }

        let previous_mode = self.current_mode.clone();
        self.current_mode = target_mode.to_string();

        info!(
            "Mode transition complete: {} -> {} (started: {}, stopped: {})",
            previous_mode,
            target_mode,
            started.len(),
            stopped.len()
        );

        Ok(ModeTransitionResult {
            previous_mode,
            current_mode: target_mode.to_string(),
            nodes_started: started,
            nodes_stopped: stopped,
            errors,
        })
    }
}

/// Calculate which nodes need to start/stop to transition to a target set of nodes
///
/// Shared by [`LifecycleManager::calculate_mode_diff`] and the CLI's
/// `CliLifecycleManager::calculate_mode_diff`, which perform the same set-diffing
/// against their own independently-tracked mode configuration and running-node state.
///
/// # Arguments
///
/// * `target_nodes` - Nodes that should be running after the transition
/// * `running_nodes` - Nodes that are currently running
///
/// # Returns
///
/// A `(start, stop)` tuple: nodes to start (in target but not running) and nodes to
/// stop (running but not in target).
pub fn diff_nodes(target_nodes: &[String], running_nodes: &HashSet<String>) -> (Vec<String>, Vec<String>) {
    let target_set: HashSet<_> = target_nodes.iter().map(|s| s.as_str()).collect();

    let start: Vec<_> = target_set
        .iter()
        .filter(|n| !running_nodes.contains(**n))
        .map(|s| s.to_string())
        .collect();

    let stop: Vec<_> = running_nodes
        .iter()
        .filter(|n| !target_set.contains(n.as_str()))
        .cloned()
        .collect();

    (start, stop)
}

#[derive(Debug)]
pub struct ModeDiff {
    pub start: Vec<String>,

    pub stop: Vec<String>,
}

#[derive(Debug, Clone)]
pub struct ModeTransitionResult {
    pub previous_mode: String,

    pub current_mode: String,

    pub nodes_started: Vec<String>,

    pub nodes_stopped: Vec<String>,

    pub errors: Vec<String>,
}