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}