1use std::collections::{HashMap, HashSet, 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::monitor::FlowExitReason;
12use super::process::{FlowId, FlowOutcome, RestartPolicy};
13use super::runtime::RuntimeSpawner;
14use super::sync_lock;
15
16const DEFAULT_MAX_RESTARTS: u32 = 3;
21const DEFAULT_MAX_PERIOD: Duration = Duration::from_secs(5);
22
23#[derive(Clone, Debug)]
32pub struct ChildSpec {
33 pub name: String,
34 pub function: u32,
35 pub args: Vec<Value>,
36 pub restart: RestartPolicy,
37}
38
39impl ChildSpec {
40 pub fn new(name: impl Into<String>, function: u32) -> Self {
41 ChildSpec {
42 name: name.into(),
43 function,
44 args: Vec::new(),
45 restart: RestartPolicy::OnFailure,
46 }
47 }
48
49 pub fn args(mut self, args: Vec<Value>) -> Self {
50 self.args = args;
51 self
52 }
53
54 pub fn restart(mut self, restart: RestartPolicy) -> Self {
55 self.restart = restart;
56 self
57 }
58}
59
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66pub enum RestartStrategy {
67 OneForOne,
69 OneForAll,
72 RestForOne,
75}
76
77#[derive(Clone, Debug)]
79pub struct SupervisorConfig {
80 pub max_restarts: u32,
84 pub max_period: Duration,
85 pub strategy: RestartStrategy,
86}
87
88impl Default for SupervisorConfig {
89 fn default() -> Self {
90 SupervisorConfig {
91 max_restarts: DEFAULT_MAX_RESTARTS,
92 max_period: DEFAULT_MAX_PERIOD,
93 strategy: RestartStrategy::OneForOne,
94 }
95 }
96}
97
98struct ChildExit {
99 id: FlowId,
100 outcome: FlowOutcome,
101}
102
103struct LiveChild {
104 spec: ChildSpec,
105 expected_shutdown: bool,
108}
109
110struct ChildTable {
111 by_id: HashMap<FlowId, LiveChild>,
112 order: Vec<FlowId>,
113}
114
115impl ChildTable {
116 fn new() -> Self {
117 Self {
118 by_id: HashMap::new(),
119 order: Vec::new(),
120 }
121 }
122
123 fn insert(&mut self, id: FlowId, child: LiveChild) {
124 self.order.push(id);
125 self.by_id.insert(id, child);
126 }
127
128 fn remove(&mut self, id: FlowId) -> Option<LiveChild> {
129 self.order.retain(|x| *x != id);
130 self.by_id.remove(&id)
131 }
132
133 fn len(&self) -> usize {
134 self.by_id.len()
135 }
136}
137
138struct Cascade {
140 waiting: HashSet<FlowId>,
141 specs: Vec<ChildSpec>,
142}
143
144struct Inner {
145 spawner: RuntimeSpawner,
146 config: SupervisorConfig,
147 events: Mutex<VecDeque<ChildExit>>,
148 cvar: Condvar,
149 children: Mutex<ChildTable>,
150 cascade: Mutex<Option<Cascade>>,
151 restart_times: Mutex<VecDeque<Instant>>,
152 intensity_exceeded: AtomicBool,
153 shutdown: AtomicBool,
154}
155
156#[derive(Clone)]
160pub(crate) struct SupervisorLink {
161 inner: Arc<Inner>,
162}
163
164impl SupervisorLink {
165 pub(crate) fn notify(&self, id: FlowId, outcome: FlowOutcome) {
166 match sync_lock::lock(&self.inner.events, "SupervisorLink::notify") {
167 Ok(mut events) => {
168 events.push_back(ChildExit { id, outcome });
169 self.inner.cvar.notify_one();
170 }
171 Err(e) => report_fault(e),
172 }
173 }
174}
175
176pub struct Supervisor {
191 inner: Arc<Inner>,
192 thread: Option<JoinHandle<()>>,
193}
194
195impl Supervisor {
196 pub fn new(spawner: RuntimeSpawner) -> Result<Self, SpawnError> {
197 Self::with_config(spawner, SupervisorConfig::default())
198 }
199
200 pub fn with_config(spawner: RuntimeSpawner, config: SupervisorConfig) -> Result<Self, SpawnError> {
204 let inner = Arc::new(Inner {
205 spawner,
206 config,
207 events: Mutex::new(VecDeque::new()),
208 cvar: Condvar::new(),
209 children: Mutex::new(ChildTable::new()),
210 cascade: Mutex::new(None),
211 restart_times: Mutex::new(VecDeque::new()),
212 intensity_exceeded: AtomicBool::new(false),
213 shutdown: AtomicBool::new(false),
214 });
215 let drive_inner = inner.clone();
216 let thread = std::thread::Builder::new()
217 .name("byteflow-supervisor".into())
218 .spawn(move || drive(drive_inner))
219 .map_err(|e| SpawnError::ThreadSpawnFailed(e.to_string()))?;
220 Ok(Supervisor {
221 inner,
222 thread: Some(thread),
223 })
224 }
225
226 pub fn start_child(&self, spec: ChildSpec) -> Result<FlowHandle, SpawnError> {
230 spawn_child(&self.inner, spec)
231 }
232
233 pub fn live_children(&self) -> usize {
234 match sync_lock::lock(&self.inner.children, "Supervisor::live_children") {
235 Ok(g) => g.len(),
236 Err(e) => {
237 report_fault(e);
238 0
239 }
240 }
241 }
242
243 pub fn intensity_exceeded(&self) -> bool {
248 self.inner.intensity_exceeded.load(Ordering::Acquire)
249 }
250
251 pub fn shutdown(mut self) {
254 self.inner.shutdown.store(true, Ordering::Release);
255 self.inner.cvar.notify_all();
256 if let Some(t) = self.thread.take() {
257 let _ = t.join();
258 }
259 }
260}
261
262fn spawn_child(inner: &Arc<Inner>, spec: ChildSpec) -> Result<FlowHandle, SpawnError> {
263 let link = SupervisorLink {
264 inner: inner.clone(),
265 };
266 let mut children = match sync_lock::lock(&inner.children, "spawn_child") {
270 Ok(c) => c,
271 Err(e) => {
272 report_fault(e);
273 return Err(SpawnError::VmInit(
274 "supervisor child table poisoned".into(),
275 ));
276 }
277 };
278 let handle = inner.spawner.spawn_linked(
279 spec.function,
280 &spec.args,
281 spec.restart,
282 link,
283 )?;
284 if !spec.name.is_empty() {
285 if let Err(e) = register_child_name(inner, handle.id(), &spec.name) {
286 inner
287 .spawner
288 .request_kill(handle.id(), FlowExitReason::Supervisor);
289 return Err(e);
290 }
291 }
292 children.insert(
293 handle.id(),
294 LiveChild {
295 spec,
296 expected_shutdown: false,
297 },
298 );
299 Ok(handle)
300}
301
302fn register_child_name(inner: &Inner, id: FlowId, name: &str) -> Result<(), SpawnError> {
303 let cap = inner
304 .spawner
305 .shared
306 .caps
307 .mint(id, super::capability::CapRights::SEND_ASK)
308 .map_err(|e| {
309 report_fault(e);
310 SpawnError::VmInit("cap mint failed (poisoned lock)".into())
311 })?;
312 match inner.spawner.shared.registry.register(
313 super::registry::RegistryName::from(name),
314 cap,
315 id,
316 ) {
317 Ok(Ok(())) => Ok(()),
318 Ok(Err(super::error::LifecycleError::AlreadyRegistered)) => {
319 Err(SpawnError::NameTaken {
320 name: name.to_string(),
321 })
322 }
323 Ok(Err(e)) => Err(SpawnError::VmInit(e.to_string())),
324 Err(e) => {
325 report_fault(e);
326 Err(SpawnError::VmInit(
327 "registry register failed (poisoned lock)".into(),
328 ))
329 }
330 }
331}
332
333fn should_restart(policy: RestartPolicy, outcome: &FlowOutcome) -> bool {
334 match policy {
335 RestartPolicy::Always => true,
336 RestartPolicy::OnFailure => matches!(outcome, FlowOutcome::Failed(_)),
337 RestartPolicy::Never => false,
338 }
339}
340
341fn intensity_hit(inner: &Inner) -> bool {
342 let now = Instant::now();
343 let mut times = match sync_lock::lock(&inner.restart_times, "intensity_hit") {
344 Ok(t) => t,
345 Err(e) => {
346 report_fault(e);
347 return true;
348 }
349 };
350 times.push_back(now);
351 let window_start = match now.checked_sub(inner.config.max_period) {
352 Some(t) => t,
353 None => now,
354 };
355 loop {
356 match times.front() {
357 Some(t) if *t < window_start => {
358 times.pop_front();
359 }
360 _ => break,
361 }
362 }
363 if times.len() as u32 > inner.config.max_restarts {
364 inner.intensity_exceeded.store(true, Ordering::Release);
365 true
366 } else {
367 false
368 }
369}
370
371fn drive(inner: Arc<Inner>) {
372 loop {
373 if inner.shutdown.load(Ordering::Acquire) {
374 return;
375 }
376 let exit = {
377 let mut events = match sync_lock::lock(&inner.events, "supervisor::drive") {
378 Ok(e) => e,
379 Err(e) => {
380 report_fault(e);
381 return;
382 }
383 };
384 loop {
385 if inner.shutdown.load(Ordering::Acquire) {
386 return;
387 }
388 if let Some(exit) = events.pop_front() {
389 break exit;
390 }
391 match sync_lock::wait_timeout(
392 &inner.cvar,
393 events,
394 Duration::from_millis(100),
395 "supervisor::wait",
396 ) {
397 Ok((guard, _)) => events = guard,
398 Err(e) => {
399 report_fault(e);
400 return;
401 }
402 }
403 }
404 };
405 handle_exit(&inner, exit);
406 }
407}
408
409fn handle_exit(inner: &Arc<Inner>, exit: ChildExit) {
410 let (spec, expected, failed_idx, later) = {
411 let mut children = match sync_lock::lock(&inner.children, "handle_exit") {
412 Ok(c) => c,
413 Err(e) => {
414 report_fault(e);
415 return;
416 }
417 };
418 let failed_idx = children.order.iter().position(|id| *id == exit.id);
419 let live = match children.remove(exit.id) {
420 Some(live) => live,
421 None => return,
422 };
423 let later = match (inner.config.strategy, failed_idx) {
424 (RestartStrategy::OneForAll, _) => children.order.clone(),
425 (RestartStrategy::RestForOne, Some(i)) => children.order[i..].to_vec(),
426 _ => Vec::new(),
427 };
428 (live.spec, live.expected_shutdown, failed_idx, later)
429 };
430
431 if expected {
432 on_cascade_progress(inner, exit.id);
433 return;
434 }
435
436 if !should_restart(spec.restart, &exit.outcome) {
437 return;
438 }
439 if inner.intensity_exceeded.load(Ordering::Acquire) || intensity_hit(inner) {
440 return;
441 }
442
443 match inner.config.strategy {
444 RestartStrategy::OneForOne => {
445 let _ = spawn_child(inner, spec);
446 }
447 RestartStrategy::OneForAll | RestartStrategy::RestForOne => {
448 start_cascade(inner, spec, failed_idx, later);
449 }
450 }
451}
452
453fn start_cascade(
454 inner: &Arc<Inner>,
455 failed: ChildSpec,
456 failed_idx: Option<usize>,
457 later: Vec<FlowId>,
458) {
459 let specs = {
460 let mut children = match sync_lock::lock(&inner.children, "start_cascade") {
461 Ok(c) => c,
462 Err(e) => {
463 report_fault(e);
464 return;
465 }
466 };
467 let later_specs: Vec<ChildSpec> = later
468 .iter()
469 .filter_map(|id| {
470 children.by_id.get_mut(id).map(|c| {
471 c.expected_shutdown = true;
472 c.spec.clone()
473 })
474 })
475 .collect();
476 match inner.config.strategy {
477 RestartStrategy::OneForAll => {
478 let mut specs = later_specs;
479 specs.insert(failed_idx.unwrap_or(specs.len()), failed);
480 specs
481 }
482 RestartStrategy::RestForOne => {
483 let mut specs = vec![failed];
484 specs.extend(later_specs);
485 specs
486 }
487 RestartStrategy::OneForOne => vec![failed],
488 }
489 };
490
491 let waiting: HashSet<FlowId> = later.iter().copied().collect();
492 match sync_lock::lock(&inner.cascade, "start_cascade.cascade") {
493 Ok(mut slot) => *slot = Some(Cascade {
494 waiting: waiting.clone(),
495 specs,
496 }),
497 Err(e) => {
498 report_fault(e);
499 return;
500 }
501 }
502
503 for id in &later {
504 inner.spawner.request_kill(*id, FlowExitReason::Supervisor);
505 }
506
507 let remaining = match sync_lock::lock(&inner.cascade, "start_cascade.prune") {
508 Ok(mut slot) => {
509 if let Some(c) = slot.as_mut() {
510 c.waiting
511 .retain(|id| matches!(inner.spawner.shared.directory.lookup(*id), Ok(Some(_))));
512 }
513 slot.as_ref().map(|c| c.waiting.is_empty()).unwrap_or(true)
514 }
515 Err(e) => {
516 report_fault(e);
517 return;
518 }
519 };
520 if remaining || waiting.is_empty() {
521 finish_cascade(inner);
522 }
523}
524
525fn on_cascade_progress(inner: &Arc<Inner>, id: FlowId) {
526 let ready = match sync_lock::lock(&inner.cascade, "on_cascade_progress") {
527 Ok(mut slot) => {
528 let Some(c) = slot.as_mut() else {
529 return;
530 };
531 c.waiting.remove(&id);
532 c.waiting.is_empty()
533 }
534 Err(e) => {
535 report_fault(e);
536 return;
537 }
538 };
539 if ready {
540 finish_cascade(inner);
541 }
542}
543
544fn finish_cascade(inner: &Arc<Inner>) {
545 let specs = match sync_lock::lock(&inner.cascade, "finish_cascade") {
546 Ok(mut slot) => match slot.take() {
547 Some(c) => c.specs,
548 None => return,
549 },
550 Err(e) => {
551 report_fault(e);
552 return;
553 }
554 };
555 for spec in specs {
556 if inner.intensity_exceeded.load(Ordering::Acquire) {
557 return;
558 }
559 let _ = spawn_child(inner, spec);
560 }
561}
562
563#[cfg(test)]
564mod tests {
565 use super::*;
566 use std::time::{Duration, Instant};
567
568 use crate::bytecode::{builder::ChunkBuilder, Chunk, Value};
569 use crate::scheduler::runtime::{Runtime, RuntimeConfig};
570
571 fn trap_chunk() -> Chunk {
572 let mut b = ChunkBuilder::new("trap");
573 b.begin_function("boom", 0, 1);
574 b.emit_trap(1);
575 b.finish()
576 }
577
578 fn ok_chunk() -> Chunk {
579 let mut b = ChunkBuilder::new("ok");
580 b.begin_function("main", 0, 1);
581 b.emit_load_imm(0, 7);
582 b.emit_return(0);
583 b.finish()
584 }
585
586 fn tiny_runtime(chunk: Chunk) -> Result<Runtime, crate::scheduler::SpawnError> {
587 Runtime::with_config(
588 chunk,
589 RuntimeConfig {
590 workers: 1,
591 quantum: 1_000,
592 mailbox: super::super::mailbox::MailboxConfig::DEFAULT,
593 ..Default::default()
594 },
595 )
596 }
597
598 fn wait_until(mut pred: impl FnMut() -> bool) {
599 let start = Instant::now();
600 while !pred() {
601 assert!(
602 start.elapsed() < Duration::from_secs(2),
603 "supervisor test timed out"
604 );
605 std::thread::sleep(Duration::from_millis(5));
606 }
607 }
608
609 #[test]
610 fn on_failure_does_not_restart_a_clean_exit() -> Result<(), Box<dyn std::error::Error>> {
611 let rt = tiny_runtime(ok_chunk())?;
612 let sup = Supervisor::new(rt.spawner())?;
613 let outcome = sup
614 .start_child(ChildSpec::new("main", 0).restart(RestartPolicy::OnFailure))?
615 .join();
616 wait_until(|| sup.live_children() == 0);
617 let spawned = rt.metrics().processes_spawned;
618 sup.shutdown();
619 rt.shutdown();
620 assert!(matches!(outcome, FlowOutcome::Completed(_)));
621 assert_eq!(spawned, 1);
622 Ok(())
623 }
624
625 #[test]
626 fn on_failure_restarts_until_intensity() -> Result<(), Box<dyn std::error::Error>> {
627 let rt = tiny_runtime(trap_chunk())?;
628 let sup = Supervisor::with_config(
629 rt.spawner(),
630 SupervisorConfig {
631 max_restarts: 2,
632 max_period: Duration::from_secs(5),
633 strategy: RestartStrategy::OneForOne,
634 },
635 )?;
636 let _first = sup
637 .start_child(ChildSpec::new("boom", 0).restart(RestartPolicy::OnFailure))?;
638 wait_until(|| sup.intensity_exceeded() && rt.metrics().processes_failed >= 3);
639 let spawned = rt.metrics().processes_spawned;
640 let failed = rt.metrics().processes_failed;
641 sup.shutdown();
642 rt.shutdown();
643 assert_eq!(spawned, 3);
645 assert_eq!(failed, 3);
646 Ok(())
647 }
648
649 #[test]
650 fn always_restarts_a_clean_exit_until_intensity() -> Result<(), Box<dyn std::error::Error>> {
651 let rt = tiny_runtime(ok_chunk())?;
652 let sup = Supervisor::with_config(
653 rt.spawner(),
654 SupervisorConfig {
655 max_restarts: 2,
656 max_period: Duration::from_secs(5),
657 strategy: RestartStrategy::OneForOne,
658 },
659 )?;
660 let _ = sup
661 .start_child(ChildSpec::new("main", 0).restart(RestartPolicy::Always))?;
662 wait_until(|| sup.intensity_exceeded() && rt.metrics().processes_completed >= 3);
663 let spawned = rt.metrics().processes_spawned;
664 sup.shutdown();
665 rt.shutdown();
666 assert_eq!(spawned, 3);
667 Ok(())
668 }
669
670 fn wait_and_trap_chunk() -> Chunk {
671 let mut b = ChunkBuilder::new("sup-mix");
672 b.begin_function("wait", 0, 1);
673 b.emit_receive(0);
674 b.emit_return(0);
675 b.begin_function("boom", 0, 1);
676 b.emit_trap(1);
677 b.begin_function("delayed", 0, 1);
678 b.emit_load_imm(0, 80);
679 b.emit_sleep(0);
680 b.emit_trap(1);
681 b.finish()
682 }
683
684 #[test]
685 fn one_for_one_does_not_kill_sibling() -> Result<(), Box<dyn std::error::Error>> {
686 let rt = tiny_runtime(wait_and_trap_chunk())?;
687 let sup = Supervisor::new(rt.spawner())?;
688 let parked = sup.start_child(ChildSpec::new("wait", 0))?;
689 let _boom = sup.start_child(ChildSpec::new("boom", 1).restart(RestartPolicy::OnFailure))?;
690 wait_until(|| rt.metrics().processes_failed >= 1);
691 assert!(parked.try_join().is_none(), "one-for-one must leave the parked sibling");
692 sup.shutdown();
693 rt.shutdown();
694 Ok(())
695 }
696
697 #[test]
698 fn one_for_all_kills_parked_sibling() -> Result<(), Box<dyn std::error::Error>> {
699 let rt = tiny_runtime(wait_and_trap_chunk())?;
700 let sup = Supervisor::with_config(
701 rt.spawner(),
702 SupervisorConfig {
703 max_restarts: 8,
704 max_period: Duration::from_secs(5),
705 strategy: RestartStrategy::OneForAll,
706 },
707 )?;
708 let parked = sup.start_child(ChildSpec::new("wait", 0))?;
709 let _boom = sup.start_child(ChildSpec::new("boom", 1).restart(RestartPolicy::OnFailure))?;
710 wait_until(|| parked.try_join().is_some());
711 let outcome = parked.join();
712 sup.shutdown();
713 rt.shutdown();
714 assert!(
715 matches!(outcome, FlowOutcome::Failed(_)),
716 "one-for-all must kill the parked sibling, got {outcome:?}"
717 );
718 Ok(())
719 }
720
721 #[test]
722 fn rest_for_one_kills_only_later_children() -> Result<(), Box<dyn std::error::Error>> {
723 let rt = tiny_runtime(wait_and_trap_chunk())?;
724 let sup = Supervisor::with_config(
725 rt.spawner(),
726 SupervisorConfig {
727 max_restarts: 8,
728 max_period: Duration::from_secs(5),
729 strategy: RestartStrategy::RestForOne,
730 },
731 )?;
732 let earlier = sup.start_child(ChildSpec::new("keep", 0))?;
733 let _boom = sup
734 .start_child(ChildSpec::new("delayed", 2).restart(RestartPolicy::OnFailure))?;
735 let later = sup.start_child(ChildSpec::new("tail", 0))?;
736 wait_until(|| later.try_join().is_some());
737 assert!(
738 earlier.try_join().is_none(),
739 "rest-for-one must not kill children started before the failure"
740 );
741 let later_outcome = later.join();
742 sup.shutdown();
743 rt.shutdown();
744 assert!(
745 matches!(later_outcome, FlowOutcome::Failed(_)),
746 "rest-for-one must kill children started after the failure, got {later_outcome:?}"
747 );
748 Ok(())
749 }
750
751 #[test]
752 fn child_name_registers_and_rejects_duplicate() -> Result<(), Box<dyn std::error::Error>> {
753 let rt = tiny_runtime(wait_and_trap_chunk())?;
754 let sup = Supervisor::new(rt.spawner())?;
755 let _parked = sup.start_child(ChildSpec::new("svc", 0))?;
756 wait_until(|| matches!(rt.whereis("svc"), Ok(Some(_))));
757 assert!(rt.whereis("svc")?.is_some());
758 let dup = sup.start_child(ChildSpec::new("svc", 1));
759 sup.shutdown();
760 rt.shutdown();
761 match dup {
762 Err(SpawnError::NameTaken { name }) if name == "svc" => {}
763 Ok(_) => return Err("expected NameTaken, got Ok(handle)".into()),
764 Err(e) => return Err(format!("expected NameTaken, got Err({e})").into()),
765 }
766 Ok(())
767 }
768
769 #[test]
770 fn policy_table() {
771 let ok = FlowOutcome::Completed(Value::Unit);
772 let fail = FlowOutcome::Failed("boom".into());
773 assert!(should_restart(RestartPolicy::Always, &ok));
774 assert!(should_restart(RestartPolicy::Always, &fail));
775 assert!(!should_restart(RestartPolicy::OnFailure, &ok));
776 assert!(should_restart(RestartPolicy::OnFailure, &fail));
777 assert!(!should_restart(RestartPolicy::Never, &ok));
778 assert!(!should_restart(RestartPolicy::Never, &fail));
779 }
780}