mecha10_runtime/
supervisor.rs1use 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#[derive(Clone, Debug, PartialEq)]
14pub enum NodeStatus {
15 Starting,
17
18 Running,
20
21 ShuttingDown,
23
24 Stopped,
26
27 Failed { reason: String },
29
30 Restarting { attempt: usize },
32}
33
34pub struct NodeHandle {
36 name: String,
37 task: JoinHandle<()>,
38}
39
40impl NodeHandle {
41 pub fn name(&self) -> &str {
43 &self.name
44 }
45
46 pub fn is_finished(&self) -> bool {
48 self.task.is_finished()
49 }
50
51 pub fn abort(&self) {
53 self.task.abort();
54 }
55
56 pub async fn wait(self) -> Result<()> {
58 self.task.await?;
59 Ok(())
60 }
61}
62
63pub struct Supervisor {
65 health_checker: Arc<HealthChecker>,
66 shutdown: ShutdownHandle,
67 restart_policy: RestartPolicy,
68 nodes: Arc<tokio::sync::RwLock<std::collections::HashMap<String, NodeHandle>>>,
70}
71
72impl Supervisor {
73 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 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 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 pub fn health_checker(&self) -> &Arc<HealthChecker> {
177 &self.health_checker
178 }
179
180 pub fn shutdown(&self) -> &ShutdownHandle {
182 &self.shutdown
183 }
184
185 #[allow(dead_code)]
198 pub async fn start_node(&self, name: &str) -> Result<()> {
199 tracing::info!("Starting node: {}", name);
200
201 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 #[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 #[allow(dead_code)]
247 pub async fn get_running_nodes(&self) -> Vec<String> {
248 self.nodes.cloned_keys().await
249 }
250}
251
252#[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}