1use std::collections::{HashMap, VecDeque};
2use std::sync::atomic::{AtomicBool, Ordering};
3use std::sync::{Arc, Condvar, Mutex};
4use std::thread::JoinHandle;
5use std::time::{Duration, Instant};
6
7use crate::bytecode::Value;
8
9use super::error::{report_fault, SpawnError};
10use super::handle::FlowHandle;
11use super::process::{FlowId, FlowOutcome, RestartPolicy};
12use super::runtime::RuntimeSpawner;
13use super::sync_lock;
14
15const DEFAULT_MAX_RESTARTS: u32 = 3;
20const DEFAULT_MAX_PERIOD: Duration = Duration::from_secs(5);
21
22#[derive(Clone, Debug)]
28pub struct ChildSpec {
29 pub name: String,
30 pub function: u32,
31 pub args: Vec<Value>,
32 pub restart: RestartPolicy,
33}
34
35impl ChildSpec {
36 pub fn new(name: impl Into<String>, function: u32) -> Self {
37 ChildSpec {
38 name: name.into(),
39 function,
40 args: Vec::new(),
41 restart: RestartPolicy::OnFailure,
42 }
43 }
44
45 pub fn args(mut self, args: Vec<Value>) -> Self {
46 self.args = args;
47 self
48 }
49
50 pub fn restart(mut self, restart: RestartPolicy) -> Self {
51 self.restart = restart;
52 self
53 }
54}
55
56#[derive(Clone, Debug)]
63pub struct SupervisorConfig {
64 pub max_restarts: u32,
68 pub max_period: Duration,
69}
70
71impl Default for SupervisorConfig {
72 fn default() -> Self {
73 SupervisorConfig {
74 max_restarts: DEFAULT_MAX_RESTARTS,
75 max_period: DEFAULT_MAX_PERIOD,
76 }
77 }
78}
79
80struct ChildExit {
81 id: FlowId,
82 outcome: FlowOutcome,
83}
84
85struct LiveChild {
86 spec: ChildSpec,
87}
88
89struct Inner {
90 spawner: RuntimeSpawner,
91 config: SupervisorConfig,
92 events: Mutex<VecDeque<ChildExit>>,
93 cvar: Condvar,
94 children: Mutex<HashMap<FlowId, LiveChild>>,
95 restart_times: Mutex<VecDeque<Instant>>,
96 intensity_exceeded: AtomicBool,
97 shutdown: AtomicBool,
98}
99
100#[derive(Clone)]
104pub(crate) struct SupervisorLink {
105 inner: Arc<Inner>,
106}
107
108impl SupervisorLink {
109 pub(crate) fn notify(&self, id: FlowId, outcome: FlowOutcome) {
110 match sync_lock::lock(&self.inner.events, "SupervisorLink::notify") {
111 Ok(mut events) => {
112 events.push_back(ChildExit { id, outcome });
113 self.inner.cvar.notify_one();
114 }
115 Err(e) => report_fault(e),
116 }
117 }
118}
119
120pub struct Supervisor {
135 inner: Arc<Inner>,
136 thread: Option<JoinHandle<()>>,
137}
138
139impl Supervisor {
140 pub fn new(spawner: RuntimeSpawner) -> Result<Self, SpawnError> {
141 Self::with_config(spawner, SupervisorConfig::default())
142 }
143
144 pub fn with_config(spawner: RuntimeSpawner, config: SupervisorConfig) -> Result<Self, SpawnError> {
148 let inner = Arc::new(Inner {
149 spawner,
150 config,
151 events: Mutex::new(VecDeque::new()),
152 cvar: Condvar::new(),
153 children: Mutex::new(HashMap::new()),
154 restart_times: Mutex::new(VecDeque::new()),
155 intensity_exceeded: AtomicBool::new(false),
156 shutdown: AtomicBool::new(false),
157 });
158 let drive_inner = inner.clone();
159 let thread = std::thread::Builder::new()
160 .name("byteflow-supervisor".into())
161 .spawn(move || drive(drive_inner))
162 .map_err(|e| SpawnError::ThreadSpawnFailed(e.to_string()))?;
163 Ok(Supervisor {
164 inner,
165 thread: Some(thread),
166 })
167 }
168
169 pub fn start_child(&self, spec: ChildSpec) -> Result<FlowHandle, SpawnError> {
173 spawn_child(&self.inner, spec)
174 }
175
176 pub fn live_children(&self) -> usize {
177 match sync_lock::lock(&self.inner.children, "Supervisor::live_children") {
178 Ok(g) => g.len(),
179 Err(e) => {
180 report_fault(e);
181 0
182 }
183 }
184 }
185
186 pub fn intensity_exceeded(&self) -> bool {
191 self.inner.intensity_exceeded.load(Ordering::Acquire)
192 }
193
194 pub fn shutdown(mut self) {
197 self.inner.shutdown.store(true, Ordering::Release);
198 self.inner.cvar.notify_all();
199 if let Some(t) = self.thread.take() {
200 let _ = t.join();
201 }
202 }
203}
204
205fn spawn_child(inner: &Arc<Inner>, spec: ChildSpec) -> Result<FlowHandle, SpawnError> {
206 let link = SupervisorLink {
207 inner: inner.clone(),
208 };
209 let mut children = match sync_lock::lock(&inner.children, "spawn_child") {
213 Ok(c) => c,
214 Err(e) => {
215 report_fault(e);
216 return Err(SpawnError::VmInit(
217 "supervisor child table poisoned".into(),
218 ));
219 }
220 };
221 let handle = inner.spawner.spawn_linked(
222 spec.function,
223 &spec.args,
224 spec.restart,
225 link,
226 )?;
227 children.insert(handle.id(), LiveChild { spec });
228 Ok(handle)
229}
230
231fn should_restart(policy: RestartPolicy, outcome: &FlowOutcome) -> bool {
232 match policy {
233 RestartPolicy::Always => true,
234 RestartPolicy::OnFailure => matches!(outcome, FlowOutcome::Failed(_)),
235 RestartPolicy::Never => false,
236 }
237}
238
239fn intensity_hit(inner: &Inner) -> bool {
240 let now = Instant::now();
241 let mut times = match sync_lock::lock(&inner.restart_times, "intensity_hit") {
242 Ok(t) => t,
243 Err(e) => {
244 report_fault(e);
245 return true;
246 }
247 };
248 times.push_back(now);
249 let window_start = match now.checked_sub(inner.config.max_period) {
250 Some(t) => t,
251 None => now,
252 };
253 loop {
254 match times.front() {
255 Some(t) if *t < window_start => {
256 times.pop_front();
257 }
258 _ => break,
259 }
260 }
261 if times.len() as u32 > inner.config.max_restarts {
262 inner.intensity_exceeded.store(true, Ordering::Release);
263 true
264 } else {
265 false
266 }
267}
268
269fn drive(inner: Arc<Inner>) {
270 loop {
271 if inner.shutdown.load(Ordering::Acquire) {
272 return;
273 }
274 let exit = {
275 let mut events = match sync_lock::lock(&inner.events, "supervisor::drive") {
276 Ok(e) => e,
277 Err(e) => {
278 report_fault(e);
279 return;
280 }
281 };
282 loop {
283 if inner.shutdown.load(Ordering::Acquire) {
284 return;
285 }
286 if let Some(exit) = events.pop_front() {
287 break exit;
288 }
289 match sync_lock::wait_timeout(
290 &inner.cvar,
291 events,
292 Duration::from_millis(100),
293 "supervisor::wait",
294 ) {
295 Ok((guard, _)) => events = guard,
296 Err(e) => {
297 report_fault(e);
298 return;
299 }
300 }
301 }
302 };
303 handle_exit(&inner, exit);
304 }
305}
306
307fn handle_exit(inner: &Arc<Inner>, exit: ChildExit) {
308 let spec = {
309 let mut children = match sync_lock::lock(&inner.children, "handle_exit") {
310 Ok(c) => c,
311 Err(e) => {
312 report_fault(e);
313 return;
314 }
315 };
316 match children.remove(&exit.id) {
317 Some(live) => live.spec,
318 None => return,
319 }
320 };
321
322 if !should_restart(spec.restart, &exit.outcome) {
323 return;
324 }
325 if inner.intensity_exceeded.load(Ordering::Acquire) || intensity_hit(inner) {
326 return;
327 }
328
329 let _ = spawn_child(inner, spec);
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335 use std::time::{Duration, Instant};
336
337 use crate::bytecode::{Chunk, ChunkBuilder, Value};
338 use crate::scheduler::runtime::{Runtime, RuntimeConfig};
339
340 fn trap_chunk() -> Chunk {
341 let mut b = ChunkBuilder::new("trap");
342 b.begin_function("boom", 0, 1);
343 b.emit_trap(1);
344 b.finish()
345 }
346
347 fn ok_chunk() -> Chunk {
348 let mut b = ChunkBuilder::new("ok");
349 b.begin_function("main", 0, 1);
350 b.emit_load_imm(0, 7);
351 b.emit_return(0);
352 b.finish()
353 }
354
355 fn tiny_runtime(chunk: Chunk) -> Result<Runtime, crate::scheduler::SpawnError> {
356 Runtime::with_config(
357 chunk,
358 RuntimeConfig {
359 workers: 1,
360 quantum: 1_000,
361 mailbox: super::super::mailbox::MailboxConfig::DEFAULT,
362 },
363 )
364 }
365
366 fn wait_until(mut pred: impl FnMut() -> bool) {
367 let start = Instant::now();
368 while !pred() {
369 assert!(
370 start.elapsed() < Duration::from_secs(2),
371 "supervisor test timed out"
372 );
373 std::thread::sleep(Duration::from_millis(5));
374 }
375 }
376
377 #[test]
378 fn on_failure_does_not_restart_a_clean_exit() -> Result<(), Box<dyn std::error::Error>> {
379 let rt = tiny_runtime(ok_chunk())?;
380 let sup = Supervisor::new(rt.spawner())?;
381 let outcome = sup
382 .start_child(ChildSpec::new("main", 0).restart(RestartPolicy::OnFailure))?
383 .join();
384 wait_until(|| sup.live_children() == 0);
385 let spawned = rt.metrics().processes_spawned;
386 sup.shutdown();
387 rt.shutdown();
388 assert!(matches!(outcome, FlowOutcome::Completed(_)));
389 assert_eq!(spawned, 1);
390 Ok(())
391 }
392
393 #[test]
394 fn on_failure_restarts_until_intensity() -> Result<(), Box<dyn std::error::Error>> {
395 let rt = tiny_runtime(trap_chunk())?;
396 let sup = Supervisor::with_config(
397 rt.spawner(),
398 SupervisorConfig {
399 max_restarts: 2,
400 max_period: Duration::from_secs(5),
401 },
402 )?;
403 let _first = sup
404 .start_child(ChildSpec::new("boom", 0).restart(RestartPolicy::OnFailure))?;
405 wait_until(|| sup.intensity_exceeded() && rt.metrics().processes_failed >= 3);
406 let spawned = rt.metrics().processes_spawned;
407 let failed = rt.metrics().processes_failed;
408 sup.shutdown();
409 rt.shutdown();
410 assert_eq!(spawned, 3);
412 assert_eq!(failed, 3);
413 Ok(())
414 }
415
416 #[test]
417 fn always_restarts_a_clean_exit_until_intensity() -> Result<(), Box<dyn std::error::Error>> {
418 let rt = tiny_runtime(ok_chunk())?;
419 let sup = Supervisor::with_config(
420 rt.spawner(),
421 SupervisorConfig {
422 max_restarts: 2,
423 max_period: Duration::from_secs(5),
424 },
425 )?;
426 let _ = sup
427 .start_child(ChildSpec::new("main", 0).restart(RestartPolicy::Always))?;
428 wait_until(|| sup.intensity_exceeded() && rt.metrics().processes_completed >= 3);
429 let spawned = rt.metrics().processes_spawned;
430 sup.shutdown();
431 rt.shutdown();
432 assert_eq!(spawned, 3);
433 Ok(())
434 }
435
436 #[test]
437 fn policy_table() {
438 let ok = FlowOutcome::Completed(Value::Unit);
439 let fail = FlowOutcome::Failed("boom".into());
440 assert!(should_restart(RestartPolicy::Always, &ok));
441 assert!(should_restart(RestartPolicy::Always, &fail));
442 assert!(!should_restart(RestartPolicy::OnFailure, &ok));
443 assert!(should_restart(RestartPolicy::OnFailure, &fail));
444 assert!(!should_restart(RestartPolicy::Never, &ok));
445 assert!(!should_restart(RestartPolicy::Never, &fail));
446 }
447}