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
41// Note: mecha10-core types will be imported once we add the dependency
42// For now, we'll define placeholder types that will be replaced
43
44/// Lifecycle manager for mode-based node orchestration
45///
46/// Manages transitions between operational modes by starting and stopping
47/// nodes via the Supervisor.
48pub struct LifecycleManager {
49    /// Current operational mode
50    current_mode: String,
51
52    /// Set of currently running node names
53    pub(crate) running_nodes: HashSet<String>,
54
55    /// Mode configurations (mode_name -> ModeConfig)
56    mode_config: HashMap<String, ModeConfig>,
57
58    /// Supervisor for node management
59    supervisor: Arc<dyn SupervisorTrait>,
60}
61
62/// Configuration for a single mode
63///
64/// This is a simplified version that will be replaced with the actual
65/// type from mecha10-cli once we integrate.
66#[derive(Debug, Clone)]
67pub struct ModeConfig {
68    /// Nodes that should run in this mode
69    pub nodes: Vec<String>,
70
71    /// Nodes that should be stopped when entering this mode
72    pub stop_nodes: Vec<String>,
73
74    /// Description of this mode
75    pub description: Option<String>,
76}
77
78/// Trait for supervisor operations
79///
80/// This allows us to mock the supervisor for testing while
81/// using the real Supervisor in production.
82#[async_trait::async_trait]
83pub trait SupervisorTrait: Send + Sync {
84    /// Start a node by name
85    async fn start_node(&self, name: &str) -> Result<()>;
86
87    /// Stop a node by name
88    async fn stop_node(&self, name: &str) -> Result<()>;
89
90    /// Get list of running nodes
91    fn get_running_nodes(&self) -> Vec<String>;
92}
93
94impl LifecycleManager {
95    /// Create a new lifecycle manager
96    ///
97    /// # Arguments
98    ///
99    /// * `supervisor` - Supervisor instance for node management
100    /// * `mode_config` - Map of mode name to mode configuration
101    /// * `default_mode` - Initial mode to start in
102    pub fn new(
103        supervisor: Arc<dyn SupervisorTrait>,
104        mode_config: HashMap<String, ModeConfig>,
105        default_mode: String,
106    ) -> Self {
107        Self {
108            current_mode: default_mode,
109            running_nodes: HashSet::new(),
110            mode_config,
111            supervisor,
112        }
113    }
114
115    /// Get the current mode
116    pub fn current_mode(&self) -> &str {
117        &self.current_mode
118    }
119
120    /// Get running nodes
121    pub fn running_nodes(&self) -> &HashSet<String> {
122        &self.running_nodes
123    }
124
125    /// Request a mode change
126    ///
127    /// This is the main entry point for mode transitions.
128    /// It calculates the diff and executes the transition.
129    ///
130    /// # Arguments
131    ///
132    /// * `target_mode` - The mode to transition to
133    ///
134    /// # Returns
135    ///
136    /// Ok with nodes started/stopped, or Err if mode doesn't exist
137    pub async fn change_mode(&mut self, target_mode: &str) -> Result<ModeTransitionResult> {
138        info!("Mode change requested: {} -> {}", self.current_mode, target_mode);
139
140        // Validate mode exists
141        if !self.mode_config.contains_key(target_mode) {
142            let available: Vec<_> = self.mode_config.keys().map(|k| k.as_str()).collect();
143            return Err(anyhow::anyhow!(
144                "Mode '{}' not found. Available modes: {}",
145                target_mode,
146                available.join(", ")
147            ));
148        }
149
150        // Calculate diff
151        let diff = self.calculate_mode_diff(target_mode)?;
152
153        info!("Mode transition plan: stop {:?}, start {:?}", diff.stop, diff.start);
154
155        // Execute transition
156        self.execute_mode_transition(target_mode, diff).await
157    }
158
159    /// Calculate which nodes need to start/stop for a mode transition
160    pub fn calculate_mode_diff(&self, target_mode: &str) -> Result<ModeDiff> {
161        let target_config = self
162            .mode_config
163            .get(target_mode)
164            .ok_or_else(|| anyhow::anyhow!("Mode not found"))?;
165
166        // Nodes that should run in target mode
167        let target_nodes: HashSet<_> = target_config.nodes.iter().map(|s| s.as_str()).collect();
168
169        // Nodes explicitly marked to stop
170        let stop_nodes: HashSet<_> = target_config.stop_nodes.iter().map(|s| s.as_str()).collect();
171
172        // Nodes to start: in target but not currently running
173        let start: Vec<_> = target_nodes
174            .iter()
175            .filter(|n| !self.running_nodes.contains(**n))
176            .map(|s| s.to_string())
177            .collect();
178
179        // Nodes to stop: running but not in target, or explicitly in stop_nodes
180        let stop: Vec<_> = self
181            .running_nodes
182            .iter()
183            .filter(|n| !target_nodes.contains(n.as_str()) || stop_nodes.contains(n.as_str()))
184            .cloned()
185            .collect();
186
187        Ok(ModeDiff { start, stop })
188    }
189
190    /// Execute a mode transition
191    async fn execute_mode_transition(&mut self, target_mode: &str, diff: ModeDiff) -> Result<ModeTransitionResult> {
192        debug!(
193            "Executing mode transition: {} -> {} (stop: {:?}, start: {:?})",
194            self.current_mode, target_mode, diff.stop, diff.start
195        );
196
197        let mut stopped = Vec::new();
198        let mut started = Vec::new();
199        let mut errors = Vec::new();
200
201        // Stop nodes first
202        for node in &diff.stop {
203            info!("Stopping node: {}", node);
204            match self.supervisor.stop_node(node).await {
205                Ok(()) => {
206                    self.running_nodes.remove(node);
207                    stopped.push(node.clone());
208                    debug!("✓ Stopped: {}", node);
209                }
210                Err(e) => {
211                    warn!("Failed to stop node {}: {}", node, e);
212                    errors.push(format!("stop {}: {}", node, e));
213                }
214            }
215        }
216
217        // Start new nodes
218        for node in &diff.start {
219            info!("Starting node: {}", node);
220            match self.supervisor.start_node(node).await {
221                Ok(()) => {
222                    self.running_nodes.insert(node.clone());
223                    started.push(node.clone());
224                    debug!("✓ Started: {}", node);
225                }
226                Err(e) => {
227                    error!("Failed to start node {}: {}", node, e);
228                    errors.push(format!("start {}: {}", node, e));
229                }
230            }
231        }
232
233        // Update current mode
234        let previous_mode = self.current_mode.clone();
235        self.current_mode = target_mode.to_string();
236
237        info!(
238            "Mode transition complete: {} -> {} (started: {}, stopped: {})",
239            previous_mode,
240            target_mode,
241            started.len(),
242            stopped.len()
243        );
244
245        Ok(ModeTransitionResult {
246            previous_mode,
247            current_mode: target_mode.to_string(),
248            nodes_started: started,
249            nodes_stopped: stopped,
250            errors,
251        })
252    }
253}
254
255/// Difference between current and target mode
256#[derive(Debug)]
257pub struct ModeDiff {
258    /// Nodes that need to be started
259    pub start: Vec<String>,
260
261    /// Nodes that need to be stopped
262    pub stop: Vec<String>,
263}
264
265/// Result of a mode transition
266#[derive(Debug, Clone)]
267pub struct ModeTransitionResult {
268    /// Previous mode
269    pub previous_mode: String,
270
271    /// New current mode
272    pub current_mode: String,
273
274    /// Nodes that were started
275    pub nodes_started: Vec<String>,
276
277    /// Nodes that were stopped
278    pub nodes_stopped: Vec<String>,
279
280    /// Errors encountered (non-fatal)
281    pub errors: Vec<String>,
282}