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)]
13pub enum NodeStatus {
14 Starting,
15
16 Running,
17
18 ShuttingDown,
19
20 Stopped,
21
22 Failed { reason: String },
23
24 Restarting { attempt: usize },
25}
26
27pub struct NodeHandle {
28 name: String,
29 task: JoinHandle<()>,
30}
31
32impl NodeHandle {
33 pub fn name(&self) -> &str {
35 &self.name
36 }
37
38 pub fn is_finished(&self) -> bool {
40 self.task.is_finished()
41 }
42
43 pub fn abort(&self) {
45 self.task.abort();
46 }
47
48 pub async fn wait(self) -> Result<()> {
50 self.task.await?;
51 Ok(())
52 }
53}
54
55pub struct Supervisor {
56 health_checker: Arc<HealthChecker>,
57 shutdown: ShutdownHandle,
58 restart_policy: RestartPolicy,
59 nodes: Arc<tokio::sync::RwLock<std::collections::HashMap<String, NodeHandle>>>,
60}
61
62impl Supervisor {
63 pub fn new(health_checker: Arc<HealthChecker>, shutdown: ShutdownHandle, restart_policy: RestartPolicy) -> Self {
65 Self {
66 health_checker,
67 shutdown,
68 restart_policy,
69 nodes: Arc::new(tokio::sync::RwLock::new(std::collections::HashMap::new())),
70 }
71 }
72
73 pub fn launch_node(&self, mut node: Box<dyn NodeRunner>) -> NodeHandle {
75 let name = node.name().to_string();
76 let health_checker = self.health_checker.clone();
77 let shutdown = self.shutdown.clone();
78 let restart_policy = self.restart_policy.clone();
79
80 tracing::info!("Launching node: {}", name);
81
82 let task = tokio::spawn(async move {
83 let node_name = node.name().to_string();
84 let mut attempt = 0;
85
86 health_checker.register(node_name.clone(), HealthStatus::Healthy).await;
87
88 loop {
89 tracing::debug!("Starting node: {} (attempt {})", node_name, attempt);
90
91 let mut shutdown_rx = shutdown.subscribe();
92 let run_future = node.run();
93
94 tokio::select! {
95 result = run_future => {
96 match result {
97 Ok(()) => {
98 tracing::info!("Node {} completed successfully", node_name);
99 health_checker
100 .update(&node_name, HealthStatus::Healthy)
101 .await;
102 break;
103 }
104 Err(e) => {
105 tracing::error!("Node {} failed: {}", node_name, e);
106 health_checker
107 .update(
108 &node_name,
109 HealthStatus::Unhealthy {
110 reason: e.to_string(),
111 },
112 )
113 .await;
114
115 if restart_policy.should_restart(attempt) {
116 let backoff = restart_policy.backoff_duration(attempt);
117 tracing::info!(
118 "Restarting node {} in {:?} (attempt {})",
119 node_name,
120 backoff,
121 attempt + 1
122 );
123 tokio::time::sleep(backoff).await;
124 attempt += 1;
125 continue;
126 } else {
127 tracing::error!(
128 "Node {} exhausted restart attempts, giving up",
129 node_name
130 );
131 break;
132 }
133 }
134 }
135 }
136 _ = shutdown_rx.recv() => {
137 tracing::info!("Shutdown signal received, stopping node: {}", node_name);
138 if let Err(e) = node.shutdown().await {
139 tracing::error!("Error during node {} shutdown: {}", node_name, e);
140 }
141 health_checker
142 .update(&node_name, HealthStatus::Healthy)
143 .await;
144 break;
145 }
146 }
147 }
148
149 health_checker.unregister(&node_name).await;
150 tracing::debug!("Node {} supervision ended", node_name);
151 });
152
153 NodeHandle { name, task }
154 }
155
156 pub fn launch_nodes(&self, nodes: Vec<Box<dyn NodeRunner>>) -> Vec<NodeHandle> {
158 nodes.into_iter().map(|node| self.launch_node(node)).collect()
159 }
160
161 pub fn health_checker(&self) -> &Arc<HealthChecker> {
163 &self.health_checker
164 }
165
166 pub fn shutdown(&self) -> &ShutdownHandle {
168 &self.shutdown
169 }
170
171 #[allow(dead_code)]
184 pub async fn start_node(&self, name: &str) -> Result<()> {
185 tracing::info!("Starting node: {}", name);
186
187 self.health_checker
188 .register(name.to_string(), HealthStatus::Healthy)
189 .await;
190
191 tracing::debug!("Node {} registered (stub)", name);
192 Ok(())
193 }
194
195 #[allow(dead_code)]
205 pub async fn stop_node(&self, name: &str) -> Result<()> {
206 tracing::info!("Stopping node: {}", name);
207
208 let mut nodes = self.nodes.write().await;
209
210 if let Some(handle) = nodes.remove(name) {
211 handle.abort();
212
213 self.health_checker.unregister(name).await;
214
215 tracing::info!("Node {} stopped", name);
216 Ok(())
217 } else {
218 self.health_checker.unregister(name).await;
219 tracing::warn!(
220 "Node {} not found in tracked nodes, unregistered from health checker",
221 name
222 );
223 Ok(())
224 }
225 }
226
227 #[allow(dead_code)]
231 pub async fn get_running_nodes(&self) -> Vec<String> {
232 self.nodes.cloned_keys().await
233 }
234}
235
236#[async_trait::async_trait]
237impl crate::lifecycle::SupervisorTrait for Supervisor {
238 async fn start_node(&self, name: &str) -> Result<()> {
239 self.start_node(name).await
240 }
241
242 async fn stop_node(&self, name: &str) -> Result<()> {
243 self.stop_node(name).await
244 }
245
246 fn get_running_nodes(&self) -> Vec<String> {
247 vec![]
248 }
249}