Skip to main content

byteflow/scheduler/
supervisor.rs

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
16/// How many restarts OTP-style supervisors allow inside a sliding window
17/// before giving up (design notes §15). Three-in-five-seconds is the
18/// classic default: enough to absorb a flaky child, tight enough that a
19/// crash loop cannot spin the runtime forever.
20const DEFAULT_MAX_RESTARTS: u32 = 3;
21const DEFAULT_MAX_PERIOD: Duration = Duration::from_secs(5);
22
23/// A child the supervisor should start (and possibly restart).
24///
25/// `function` is an index into the runtime's chunk — the same number
26/// [`super::runtime::Runtime::spawn`] takes. Args are cloned on every
27/// restart so a child always comes back with the original call.
28///
29/// Non-empty [`Self::name`] is registered as `register_name` → a SEND|ASK
30/// Cap for that incarnation (swept on exit, re-bound on restart).
31#[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/// Which siblings die (and later come back) when one child exits.
61///
62/// Sibling abort is **cooperative**: parked children are taken out of the
63/// mailbox immediately; a child mid-quantum dies at the next budget edge
64/// (same contract as [`super::runtime::Runtime::kill`]).
65#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66pub enum RestartStrategy {
67    /// Only the child that exited is considered for restart.
68    OneForOne,
69    /// Every sibling is shut down, then the whole set is started again
70    /// in original start order.
71    OneForAll,
72    /// Children started *after* the failed one are shut down, then that
73    /// suffix (failed + later) is started again in start order.
74    RestForOne,
75}
76
77/// Tunables for [`Supervisor::with_config`].
78#[derive(Clone, Debug)]
79pub struct SupervisorConfig {
80    /// Restarts allowed inside [`Self::max_period`]. The initial start does
81    /// not count; only respawns do. Hitting this cap sets
82    /// [`Supervisor::intensity_exceeded`] and further restarts are refused.
83    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    /// Set when this incarnation is being torn down by a cascade so its
106    /// exit does not start another strategy wave.
107    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
138/// In-flight one-for-all / rest-for-one: wait for sibling kills, then respawn.
139struct 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/// Cheap, `Clone` handle the worker uses to hand a terminal outcome back
157/// without taking a lock on the supervisor's child table (the drive loop
158/// is the only writer of that table).
159#[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
176/// Host-side child restarter (design notes §15-16).
177///
178/// A `Supervisor` is **not** a bytecode Flow. It is a dedicated OS
179/// thread plus a table of [`ChildSpec`]s. When a supervised flow
180/// becomes [`FlowOutcome::Failed`] (or completes, under
181/// [`RestartPolicy::Always`]), the worker delivers the
182/// [`FlowOutcome`] here instead of letting the fault take anything
183/// else down. The supervisor then consults the child's
184/// [`RestartPolicy`] and, if intensity allows, respawns it under a
185/// fresh [`FlowId`] — Pids are never reused (see
186/// [`super::process::FlowId`]).
187///
188/// Constructed from a [`RuntimeSpawner`] so it does not have to own the
189/// runtime's worker `JoinHandle`s.
190pub 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    /// Start the dedicated supervisor OS thread. Thread-spawn failure is
201    /// [`SpawnError::ThreadSpawnFailed`] — same category-A surface as
202    /// [`super::runtime::Runtime::new`], not a panic.
203    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    /// Spawn `spec` and start supervising it. The returned handle is for
227    /// this incarnation only — a restart allocates a new Pid and a new
228    /// completion channel.
229    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    /// `true` once more than [`SupervisorConfig::max_restarts`] respawns
244    /// landed inside the intensity window. Remaining children keep
245    /// running; we just stop bringing them back (no safe abort of a
246    /// mid-quantum flow).
247    pub fn intensity_exceeded(&self) -> bool {
248        self.inner.intensity_exceeded.load(Ordering::Acquire)
249    }
250
251    /// Stop the drive thread. Does not terminate live children — they
252    /// belong to the runtime, not to us.
253    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    // Hold the table across spawn so a child that faults in its first
267    // quantum cannot notify us before its row exists (the drive loop
268    // takes this same lock in `handle_exit`, so the event waits).
269    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        // initial start + 2 restarts, then intensity refuses the 3rd restart
644        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}