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}