Skip to main content

mecha10_runtime/
supervisor.rs

1//! Node supervision and lifecycle management
2
3use crate::config::RestartPolicy;
4use crate::health::{HealthChecker, HealthStatus};
5use crate::node::NodeRunner;
6use crate::shutdown::ShutdownHandle;
7use anyhow::Result;
8use mecha10_core::rwlock_collections::RwLockMapExt;
9use std::sync::Arc;
10use tokio::task::JoinHandle;
11
12/// Status of a supervised node
13#[derive(Clone, Debug, PartialEq)]
14pub enum NodeStatus {
15    /// Node is starting up
16    Starting,
17
18    /// Node is running normally
19    Running,
20
21    /// Node is shutting down
22    ShuttingDown,
23
24    /// Node has stopped
25    Stopped,
26
27    /// Node failed with error
28    Failed { reason: String },
29
30    /// Node is restarting
31    Restarting { attempt: usize },
32}
33
34/// Handle to a supervised node
35pub struct NodeHandle {
36    name: String,
37    task: JoinHandle<()>,
38}
39
40impl NodeHandle {
41    /// Get the node name
42    pub fn name(&self) -> &str {
43        &self.name
44    }
45
46    /// Check if the node task is finished
47    pub fn is_finished(&self) -> bool {
48        self.task.is_finished()
49    }
50
51    /// Abort the node task
52    pub fn abort(&self) {
53        self.task.abort();
54    }
55
56    /// Wait for the node to finish
57    pub async fn wait(self) -> Result<()> {
58        self.task.await?;
59        Ok(())
60    }
61}
62
63/// Supervisor for managing node lifecycle
64pub struct Supervisor {
65    health_checker: Arc<HealthChecker>,
66    shutdown: ShutdownHandle,
67    restart_policy: RestartPolicy,
68    /// Tracked node handles by name
69    nodes: Arc<tokio::sync::RwLock<std::collections::HashMap<String, NodeHandle>>>,
70}
71
72impl Supervisor {
73    /// Create a new supervisor
74    pub fn new(health_checker: Arc<HealthChecker>, shutdown: ShutdownHandle, restart_policy: RestartPolicy) -> Self {
75        Self {
76            health_checker,
77            shutdown,
78            restart_policy,
79            nodes: Arc::new(tokio::sync::RwLock::new(std::collections::HashMap::new())),
80        }
81    }
82
83    /// Launch and supervise a node
84    pub fn launch_node(&self, mut node: Box<dyn NodeRunner>) -> NodeHandle {
85        let name = node.name().to_string();
86        let health_checker = self.health_checker.clone();
87        let shutdown = self.shutdown.clone();
88        let restart_policy = self.restart_policy.clone();
89
90        tracing::info!("Launching node: {}", name);
91
92        let task = tokio::spawn(async move {
93            let node_name = node.name().to_string();
94            let mut attempt = 0;
95
96            health_checker.register(node_name.clone(), HealthStatus::Healthy).await;
97
98            loop {
99                tracing::debug!("Starting node: {} (attempt {})", node_name, attempt);
100
101                let mut shutdown_rx = shutdown.subscribe();
102                let run_future = node.run();
103
104                tokio::select! {
105                    result = run_future => {
106                        match result {
107                            Ok(()) => {
108                                tracing::info!("Node {} completed successfully", node_name);
109                                health_checker
110                                    .update(&node_name, HealthStatus::Healthy)
111                                    .await;
112                                break;
113                            }
114                            Err(e) => {
115                                tracing::error!("Node {} failed: {}", node_name, e);
116                                health_checker
117                                    .update(
118                                        &node_name,
119                                        HealthStatus::Unhealthy {
120                                            reason: e.to_string(),
121                                        },
122                                    )
123                                    .await;
124
125                                if restart_policy.should_restart(attempt) {
126                                    let backoff = restart_policy.backoff_duration(attempt);
127                                    tracing::info!(
128                                        "Restarting node {} in {:?} (attempt {})",
129                                        node_name,
130                                        backoff,
131                                        attempt + 1
132                                    );
133                                    tokio::time::sleep(backoff).await;
134                                    attempt += 1;
135                                    continue;
136                                } else {
137                                    tracing::error!(
138                                        "Node {} exhausted restart attempts, giving up",
139                                        node_name
140                                    );
141                                    break;
142                                }
143                            }
144                        }
145                    }
146                    _ = shutdown_rx.recv() => {
147                        tracing::info!("Shutdown signal received, stopping node: {}", node_name);
148                        if let Err(e) = node.shutdown().await {
149                            tracing::error!("Error during node {} shutdown: {}", node_name, e);
150                        }
151                        health_checker
152                            .update(&node_name, HealthStatus::Healthy)
153                            .await;
154                        break;
155                    }
156                }
157            }
158
159            health_checker.unregister(&node_name).await;
160            tracing::debug!("Node {} supervision ended", node_name);
161        });
162
163        NodeHandle { name, task }
164    }
165
166    // duplicate: false positive — matches `ThroughputTracker::get_all_stats`
167    // (packages/core/src/profiling.rs) on AST shape only (map each item through a per-item
168    // method, collect); unrelated domains (spawning node supervision tasks vs computing
169    // per-topic throughput stats), nothing to share beyond the `map`/`collect` idiom itself.
170    /// Launch multiple nodes
171    pub fn launch_nodes(&self, nodes: Vec<Box<dyn NodeRunner>>) -> Vec<NodeHandle> {
172        nodes.into_iter().map(|node| self.launch_node(node)).collect()
173    }
174
175    /// Get the health checker
176    pub fn health_checker(&self) -> &Arc<HealthChecker> {
177        &self.health_checker
178    }
179
180    /// Get the shutdown handle
181    pub fn shutdown(&self) -> &ShutdownHandle {
182        &self.shutdown
183    }
184
185    /// Start a node by name (for lifecycle management)
186    ///
187    /// Note: This is a stub implementation for the lifecycle system.
188    /// Full implementation requires node registry or factory pattern.
189    ///
190    /// # Arguments
191    ///
192    /// * `name` - Name of the node to start
193    ///
194    /// # Errors
195    ///
196    /// Returns error if node cannot be found or started
197    #[allow(dead_code)]
198    pub async fn start_node(&self, name: &str) -> Result<()> {
199        tracing::info!("Starting node: {}", name);
200
201        // TODO: Implement node registry/factory to create nodes by name
202
203        self.health_checker
204            .register(name.to_string(), HealthStatus::Healthy)
205            .await;
206
207        tracing::debug!("Node {} registered (stub)", name);
208        Ok(())
209    }
210
211    /// Stop a node by name (for lifecycle management)
212    ///
213    /// # Arguments
214    ///
215    /// * `name` - Name of the node to stop
216    ///
217    /// # Errors
218    ///
219    /// Returns error if node cannot be found or stopped
220    #[allow(dead_code)]
221    pub async fn stop_node(&self, name: &str) -> Result<()> {
222        tracing::info!("Stopping node: {}", name);
223
224        let mut nodes = self.nodes.write().await;
225
226        if let Some(handle) = nodes.remove(name) {
227            handle.abort();
228
229            self.health_checker.unregister(name).await;
230
231            tracing::info!("Node {} stopped", name);
232            Ok(())
233        } else {
234            self.health_checker.unregister(name).await;
235            tracing::warn!(
236                "Node {} not found in tracked nodes, unregistered from health checker",
237                name
238            );
239            Ok(())
240        }
241    }
242
243    /// Get list of currently running nodes
244    ///
245    /// Returns the names of all nodes being supervised
246    #[allow(dead_code)]
247    pub async fn get_running_nodes(&self) -> Vec<String> {
248        self.nodes.cloned_keys().await
249    }
250}
251
252/// Implement the SupervisorTrait for Supervisor
253#[async_trait::async_trait]
254impl crate::lifecycle::SupervisorTrait for Supervisor {
255    async fn start_node(&self, name: &str) -> Result<()> {
256        self.start_node(name).await
257    }
258
259    async fn stop_node(&self, name: &str) -> Result<()> {
260        self.stop_node(name).await
261    }
262
263    fn get_running_nodes(&self) -> Vec<String> {
264        vec![]
265    }
266}