Skip to main content

mecha10_runtime/
lifecycle.rs

1//! Lifecycle management for mode-based dynamic node control
2//!
3//! This module provides the lifecycle manager that orchestrates node
4//! start/stop based on operational mode changes.
5//!
6//! # Architecture
7//!
8//! ```text
9//! CLI → Redis (mode/request) → LifecycleManager → Supervisor → Nodes
10//! ```
11//!
12//! The lifecycle manager:
13//! - Subscribes to mode change requests
14//! - Calculates which nodes need to start/stop
15//! - Uses Supervisor to execute changes
16//! - Publishes mode changed events
17//!
18//! # Example
19//!
20//! ```rust,ignore
21//! use mecha10_runtime::lifecycle::LifecycleManager;
22//!
23//! let lifecycle = LifecycleManager::new(
24//!     context,
25//!     supervisor,
26//!     mode_config,
27//!     "startup".to_string(),
28//! );
29//!
30//! // Run in background
31//! tokio::spawn(async move {
32//!     lifecycle.run().await
33//! });
34//! ```
35
36use anyhow::Result;
37use std::collections::{HashMap, HashSet};
38use std::sync::Arc;
39use tracing::{debug, error, info, warn};
40
41pub struct LifecycleManager {
42    current_mode: String,
43
44    pub(crate) running_nodes: HashSet<String>,
45
46    mode_config: HashMap<String, ModeConfig>,
47
48    supervisor: Arc<dyn SupervisorTrait>,
49}
50
51#[derive(Debug, Clone)]
52pub struct ModeConfig {
53    pub nodes: Vec<String>,
54
55    pub description: Option<String>,
56}
57
58#[async_trait::async_trait]
59pub trait SupervisorTrait: Send + Sync {
60    /// Start a node by name
61    async fn start_node(&self, name: &str) -> Result<()>;
62
63    /// Stop a node by name
64    async fn stop_node(&self, name: &str) -> Result<()>;
65
66    /// Get list of running nodes
67    fn get_running_nodes(&self) -> Vec<String>;
68}
69
70impl LifecycleManager {
71    /// Create a new lifecycle manager
72    ///
73    /// # Arguments
74    ///
75    /// * `supervisor` - Supervisor instance for node management
76    /// * `mode_config` - Map of mode name to mode configuration
77    /// * `default_mode` - Initial mode to start in
78    pub fn new(
79        supervisor: Arc<dyn SupervisorTrait>,
80        mode_config: HashMap<String, ModeConfig>,
81        default_mode: String,
82    ) -> Self {
83        Self {
84            current_mode: default_mode,
85            running_nodes: HashSet::new(),
86            mode_config,
87            supervisor,
88        }
89    }
90
91    /// Get the current mode
92    pub fn current_mode(&self) -> &str {
93        &self.current_mode
94    }
95
96    /// Get running nodes
97    pub fn running_nodes(&self) -> &HashSet<String> {
98        &self.running_nodes
99    }
100
101    /// Request a mode change
102    ///
103    /// This is the main entry point for mode transitions.
104    /// It calculates the diff and executes the transition.
105    ///
106    /// # Arguments
107    ///
108    /// * `target_mode` - The mode to transition to
109    ///
110    /// # Returns
111    ///
112    /// Ok with nodes started/stopped, or Err if mode doesn't exist
113    pub async fn change_mode(&mut self, target_mode: &str) -> Result<ModeTransitionResult> {
114        info!("Mode change requested: {} -> {}", self.current_mode, target_mode);
115
116        if !self.mode_config.contains_key(target_mode) {
117            let available: Vec<_> = self.mode_config.keys().map(|k| k.as_str()).collect();
118            return Err(anyhow::anyhow!(
119                "Mode '{}' not found. Available modes: {}",
120                target_mode,
121                available.join(", ")
122            ));
123        }
124
125        let diff = self.calculate_mode_diff(target_mode)?;
126
127        info!("Mode transition plan: stop {:?}, start {:?}", diff.stop, diff.start);
128
129        self.execute_mode_transition(target_mode, diff).await
130    }
131
132    /// Calculate which nodes need to start/stop for a mode transition
133    pub fn calculate_mode_diff(&self, target_mode: &str) -> Result<ModeDiff> {
134        let target_config = self
135            .mode_config
136            .get(target_mode)
137            .ok_or_else(|| anyhow::anyhow!("Mode not found"))?;
138
139        let (start, stop) = diff_nodes(&target_config.nodes, &self.running_nodes);
140
141        Ok(ModeDiff { start, stop })
142    }
143
144    /// Execute a mode transition
145    async fn execute_mode_transition(&mut self, target_mode: &str, diff: ModeDiff) -> Result<ModeTransitionResult> {
146        debug!(
147            "Executing mode transition: {} -> {} (stop: {:?}, start: {:?})",
148            self.current_mode, target_mode, diff.stop, diff.start
149        );
150
151        let mut stopped = Vec::new();
152        let mut started = Vec::new();
153        let mut errors = Vec::new();
154
155        for node in &diff.stop {
156            info!("Stopping node: {}", node);
157            match self.supervisor.stop_node(node).await {
158                Ok(()) => {
159                    self.running_nodes.remove(node);
160                    stopped.push(node.clone());
161                    debug!("✓ Stopped: {}", node);
162                }
163                Err(e) => {
164                    warn!("Failed to stop node {}: {}", node, e);
165                    errors.push(format!("stop {}: {}", node, e));
166                }
167            }
168        }
169
170        for node in &diff.start {
171            info!("Starting node: {}", node);
172            match self.supervisor.start_node(node).await {
173                Ok(()) => {
174                    self.running_nodes.insert(node.clone());
175                    started.push(node.clone());
176                    debug!("✓ Started: {}", node);
177                }
178                Err(e) => {
179                    error!("Failed to start node {}: {}", node, e);
180                    errors.push(format!("start {}: {}", node, e));
181                }
182            }
183        }
184
185        let previous_mode = self.current_mode.clone();
186        self.current_mode = target_mode.to_string();
187
188        info!(
189            "Mode transition complete: {} -> {} (started: {}, stopped: {})",
190            previous_mode,
191            target_mode,
192            started.len(),
193            stopped.len()
194        );
195
196        Ok(ModeTransitionResult {
197            previous_mode,
198            current_mode: target_mode.to_string(),
199            nodes_started: started,
200            nodes_stopped: stopped,
201            errors,
202        })
203    }
204}
205
206/// Calculate which nodes need to start/stop to transition to a target set of nodes
207///
208/// Shared by [`LifecycleManager::calculate_mode_diff`] and the CLI's
209/// `CliLifecycleManager::calculate_mode_diff`, which perform the same set-diffing
210/// against their own independently-tracked mode configuration and running-node state.
211///
212/// # Arguments
213///
214/// * `target_nodes` - Nodes that should be running after the transition
215/// * `running_nodes` - Nodes that are currently running
216///
217/// # Returns
218///
219/// A `(start, stop)` tuple: nodes to start (in target but not running) and nodes to
220/// stop (running but not in target).
221pub fn diff_nodes(target_nodes: &[String], running_nodes: &HashSet<String>) -> (Vec<String>, Vec<String>) {
222    let target_set: HashSet<_> = target_nodes.iter().map(|s| s.as_str()).collect();
223
224    let start: Vec<_> = target_set
225        .iter()
226        .filter(|n| !running_nodes.contains(**n))
227        .map(|s| s.to_string())
228        .collect();
229
230    let stop: Vec<_> = running_nodes
231        .iter()
232        .filter(|n| !target_set.contains(n.as_str()))
233        .cloned()
234        .collect();
235
236    (start, stop)
237}
238
239#[derive(Debug)]
240pub struct ModeDiff {
241    pub start: Vec<String>,
242
243    pub stop: Vec<String>,
244}
245
246#[derive(Debug, Clone)]
247pub struct ModeTransitionResult {
248    pub previous_mode: String,
249
250    pub current_mode: String,
251
252    pub nodes_started: Vec<String>,
253
254    pub nodes_stopped: Vec<String>,
255
256    pub errors: Vec<String>,
257}