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
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    /// Description of this mode
72    pub description: Option<String>,
73}
74
75/// Trait for supervisor operations
76///
77/// This allows us to mock the supervisor for testing while
78/// using the real Supervisor in production.
79#[async_trait::async_trait]
80pub trait SupervisorTrait: Send + Sync {
81    /// Start a node by name
82    async fn start_node(&self, name: &str) -> Result<()>;
83
84    /// Stop a node by name
85    async fn stop_node(&self, name: &str) -> Result<()>;
86
87    /// Get list of running nodes
88    fn get_running_nodes(&self) -> Vec<String>;
89}
90
91impl LifecycleManager {
92    /// Create a new lifecycle manager
93    ///
94    /// # Arguments
95    ///
96    /// * `supervisor` - Supervisor instance for node management
97    /// * `mode_config` - Map of mode name to mode configuration
98    /// * `default_mode` - Initial mode to start in
99    pub fn new(
100        supervisor: Arc<dyn SupervisorTrait>,
101        mode_config: HashMap<String, ModeConfig>,
102        default_mode: String,
103    ) -> Self {
104        Self {
105            current_mode: default_mode,
106            running_nodes: HashSet::new(),
107            mode_config,
108            supervisor,
109        }
110    }
111
112    /// Get the current mode
113    pub fn current_mode(&self) -> &str {
114        &self.current_mode
115    }
116
117    /// Get running nodes
118    pub fn running_nodes(&self) -> &HashSet<String> {
119        &self.running_nodes
120    }
121
122    /// Request a mode change
123    ///
124    /// This is the main entry point for mode transitions.
125    /// It calculates the diff and executes the transition.
126    ///
127    /// # Arguments
128    ///
129    /// * `target_mode` - The mode to transition to
130    ///
131    /// # Returns
132    ///
133    /// Ok with nodes started/stopped, or Err if mode doesn't exist
134    pub async fn change_mode(&mut self, target_mode: &str) -> Result<ModeTransitionResult> {
135        info!("Mode change requested: {} -> {}", self.current_mode, target_mode);
136
137        // Validate mode exists
138        if !self.mode_config.contains_key(target_mode) {
139            let available: Vec<_> = self.mode_config.keys().map(|k| k.as_str()).collect();
140            return Err(anyhow::anyhow!(
141                "Mode '{}' not found. Available modes: {}",
142                target_mode,
143                available.join(", ")
144            ));
145        }
146
147        // Calculate diff
148        let diff = self.calculate_mode_diff(target_mode)?;
149
150        info!("Mode transition plan: stop {:?}, start {:?}", diff.stop, diff.start);
151
152        // Execute transition
153        self.execute_mode_transition(target_mode, diff).await
154    }
155
156    /// Calculate which nodes need to start/stop for a mode transition
157    pub fn calculate_mode_diff(&self, target_mode: &str) -> Result<ModeDiff> {
158        let target_config = self
159            .mode_config
160            .get(target_mode)
161            .ok_or_else(|| anyhow::anyhow!("Mode not found"))?;
162
163        let (start, stop) = diff_nodes(&target_config.nodes, &self.running_nodes);
164
165        Ok(ModeDiff { start, stop })
166    }
167
168    /// Execute a mode transition
169    async fn execute_mode_transition(&mut self, target_mode: &str, diff: ModeDiff) -> Result<ModeTransitionResult> {
170        debug!(
171            "Executing mode transition: {} -> {} (stop: {:?}, start: {:?})",
172            self.current_mode, target_mode, diff.stop, diff.start
173        );
174
175        let mut stopped = Vec::new();
176        let mut started = Vec::new();
177        let mut errors = Vec::new();
178
179        // Stop nodes first
180        for node in &diff.stop {
181            info!("Stopping node: {}", node);
182            match self.supervisor.stop_node(node).await {
183                Ok(()) => {
184                    self.running_nodes.remove(node);
185                    stopped.push(node.clone());
186                    debug!("✓ Stopped: {}", node);
187                }
188                Err(e) => {
189                    warn!("Failed to stop node {}: {}", node, e);
190                    errors.push(format!("stop {}: {}", node, e));
191                }
192            }
193        }
194
195        // Start new nodes
196        for node in &diff.start {
197            info!("Starting node: {}", node);
198            match self.supervisor.start_node(node).await {
199                Ok(()) => {
200                    self.running_nodes.insert(node.clone());
201                    started.push(node.clone());
202                    debug!("✓ Started: {}", node);
203                }
204                Err(e) => {
205                    error!("Failed to start node {}: {}", node, e);
206                    errors.push(format!("start {}: {}", node, e));
207                }
208            }
209        }
210
211        // Update current mode
212        let previous_mode = self.current_mode.clone();
213        self.current_mode = target_mode.to_string();
214
215        info!(
216            "Mode transition complete: {} -> {} (started: {}, stopped: {})",
217            previous_mode,
218            target_mode,
219            started.len(),
220            stopped.len()
221        );
222
223        Ok(ModeTransitionResult {
224            previous_mode,
225            current_mode: target_mode.to_string(),
226            nodes_started: started,
227            nodes_stopped: stopped,
228            errors,
229        })
230    }
231}
232
233/// Calculate which nodes need to start/stop to transition to a target set of nodes
234///
235/// Shared by [`LifecycleManager::calculate_mode_diff`] and the CLI's
236/// `CliLifecycleManager::calculate_mode_diff`, which perform the same set-diffing
237/// against their own independently-tracked mode configuration and running-node state.
238///
239/// # Arguments
240///
241/// * `target_nodes` - Nodes that should be running after the transition
242/// * `running_nodes` - Nodes that are currently running
243///
244/// # Returns
245///
246/// A `(start, stop)` tuple: nodes to start (in target but not running) and nodes to
247/// stop (running but not in target).
248pub fn diff_nodes(target_nodes: &[String], running_nodes: &HashSet<String>) -> (Vec<String>, Vec<String>) {
249    let target_set: HashSet<_> = target_nodes.iter().map(|s| s.as_str()).collect();
250
251    // Nodes to start: in target but not currently running
252    let start: Vec<_> = target_set
253        .iter()
254        .filter(|n| !running_nodes.contains(**n))
255        .map(|s| s.to_string())
256        .collect();
257
258    // Nodes to stop: running but not in target
259    let stop: Vec<_> = running_nodes
260        .iter()
261        .filter(|n| !target_set.contains(n.as_str()))
262        .cloned()
263        .collect();
264
265    (start, stop)
266}
267
268/// Difference between current and target mode
269#[derive(Debug)]
270pub struct ModeDiff {
271    /// Nodes that need to be started
272    pub start: Vec<String>,
273
274    /// Nodes that need to be stopped
275    pub stop: Vec<String>,
276}
277
278/// Result of a mode transition
279#[derive(Debug, Clone)]
280pub struct ModeTransitionResult {
281    /// Previous mode
282    pub previous_mode: String,
283
284    /// New current mode
285    pub current_mode: String,
286
287    /// Nodes that were started
288    pub nodes_started: Vec<String>,
289
290    /// Nodes that were stopped
291    pub nodes_stopped: Vec<String>,
292
293    /// Errors encountered (non-fatal)
294    pub errors: Vec<String>,
295}