Skip to main content

byteflow/scheduler/
supervisor.rs

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
15/// How many restarts OTP-style supervisors allow inside a sliding window
16/// before giving up (design notes §15). Three-in-five-seconds is the
17/// classic default: enough to absorb a flaky child, tight enough that a
18/// crash loop cannot spin the runtime forever.
19const DEFAULT_MAX_RESTARTS: u32 = 3;
20const DEFAULT_MAX_PERIOD: Duration = Duration::from_secs(5);
21
22/// A child the supervisor should start (and possibly restart).
23///
24/// `function` is an index into the runtime's chunk — the same number
25/// [`super::runtime::Runtime::spawn`] takes. Args are cloned on every
26/// restart so a child always comes back with the original call.
27#[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/// Tunables for [`Supervisor::with_config`].
57///
58/// Unlike a full OTP supervisor this does **not** implement one-for-all /
59/// rest-for-one: those strategies require aborting sibling processes, and
60/// Byteflow's preemption is cooperative (see [`super::runtime::Runtime::shutdown`]).
61/// v0 is one-for-one — only the child that exited is considered for restart.
62#[derive(Clone, Debug)]
63pub struct SupervisorConfig {
64    /// Restarts allowed inside [`Self::max_period`]. The initial start does
65    /// not count; only respawns do. Hitting this cap sets
66    /// [`Supervisor::intensity_exceeded`] and further restarts are refused.
67    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/// Cheap, `Clone` handle the worker uses to hand a terminal outcome back
101/// without taking a lock on the supervisor's child table (the drive loop
102/// is the only writer of that table).
103#[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
120/// Host-side child restarter (design notes §15-16).
121///
122/// A `Supervisor` is **not** a bytecode Flow. It is a dedicated OS
123/// thread plus a table of [`ChildSpec`]s. When a supervised flow
124/// becomes `FlowState::Failed` (or completes, under
125/// [`RestartPolicy::Always`]), the worker delivers the
126/// [`FlowOutcome`] here instead of letting the fault take anything
127/// else down. The supervisor then consults the child's
128/// [`RestartPolicy`] and, if intensity allows, respawns it under a
129/// fresh [`FlowId`] — Pids are never reused (see
130/// [`super::process::FlowId`]).
131///
132/// Constructed from a [`RuntimeSpawner`] so it does not have to own the
133/// runtime's worker `JoinHandle`s.
134pub 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    /// Start the dedicated supervisor OS thread. Thread-spawn failure is
145    /// [`SpawnError::ThreadSpawnFailed`] — same category-A surface as
146    /// [`super::runtime::Runtime::new`], not a panic.
147    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    /// Spawn `spec` and start supervising it. The returned handle is for
170    /// this incarnation only — a restart allocates a new Pid and a new
171    /// completion channel.
172    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    /// `true` once more than [`SupervisorConfig::max_restarts`] respawns
187    /// landed inside the intensity window. Remaining children keep
188    /// running; we just stop bringing them back (no safe abort of a
189    /// mid-quantum flow).
190    pub fn intensity_exceeded(&self) -> bool {
191        self.inner.intensity_exceeded.load(Ordering::Acquire)
192    }
193
194    /// Stop the drive thread. Does not terminate live children — they
195    /// belong to the runtime, not to us.
196    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    // Hold the table across spawn so a child that faults in its first
210    // quantum cannot notify us before its row exists (the drive loop
211    // takes this same lock in `handle_exit`, so the event waits).
212    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 = now.checked_sub(inner.config.max_period).unwrap_or(now);
250    while times.front().map(|t| *t < window_start).unwrap_or(false) {
251        times.pop_front();
252    }
253    if times.len() as u32 > inner.config.max_restarts {
254        inner.intensity_exceeded.store(true, Ordering::Release);
255        true
256    } else {
257        false
258    }
259}
260
261fn drive(inner: Arc<Inner>) {
262    loop {
263        if inner.shutdown.load(Ordering::Acquire) {
264            return;
265        }
266        let exit = {
267            let mut events = match sync_lock::lock(&inner.events, "supervisor::drive") {
268                Ok(e) => e,
269                Err(e) => {
270                    report_fault(e);
271                    return;
272                }
273            };
274            loop {
275                if inner.shutdown.load(Ordering::Acquire) {
276                    return;
277                }
278                if let Some(exit) = events.pop_front() {
279                    break exit;
280                }
281                match sync_lock::wait_timeout(
282                    &inner.cvar,
283                    events,
284                    Duration::from_millis(100),
285                    "supervisor::wait",
286                ) {
287                    Ok((guard, _)) => events = guard,
288                    Err(e) => {
289                        report_fault(e);
290                        return;
291                    }
292                }
293            }
294        };
295        handle_exit(&inner, exit);
296    }
297}
298
299fn handle_exit(inner: &Arc<Inner>, exit: ChildExit) {
300    let spec = {
301        let mut children = match sync_lock::lock(&inner.children, "handle_exit") {
302            Ok(c) => c,
303            Err(e) => {
304                report_fault(e);
305                return;
306            }
307        };
308        match children.remove(&exit.id) {
309            Some(live) => live.spec,
310            None => return,
311        }
312    };
313
314    if !should_restart(spec.restart, &exit.outcome) {
315        return;
316    }
317    if inner.intensity_exceeded.load(Ordering::Acquire) || intensity_hit(inner) {
318        return;
319    }
320
321    let _ = spawn_child(inner, spec);
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327    use std::time::{Duration, Instant};
328
329    use crate::bytecode::{Chunk, ChunkBuilder, Value};
330    use crate::scheduler::runtime::{Runtime, RuntimeConfig};
331
332    fn trap_chunk() -> Chunk {
333        let mut b = ChunkBuilder::new("trap");
334        b.begin_function("boom", 0, 1);
335        b.emit_trap(1);
336        b.finish()
337    }
338
339    fn ok_chunk() -> Chunk {
340        let mut b = ChunkBuilder::new("ok");
341        b.begin_function("main", 0, 1);
342        b.emit_load_imm(0, 7);
343        b.emit_return(0);
344        b.finish()
345    }
346
347    fn tiny_runtime(chunk: Chunk) -> Runtime {
348        Runtime::with_config(
349            chunk,
350            RuntimeConfig {
351                workers: 1,
352                quantum: 1_000,
353            },
354        )
355        .expect("runtime")
356    }
357
358    fn wait_until(mut pred: impl FnMut() -> bool) {
359        let start = Instant::now();
360        while !pred() {
361            assert!(
362                start.elapsed() < Duration::from_secs(2),
363                "supervisor test timed out"
364            );
365            std::thread::sleep(Duration::from_millis(5));
366        }
367    }
368
369    #[test]
370    fn on_failure_does_not_restart_a_clean_exit() {
371        let rt = tiny_runtime(ok_chunk());
372        let sup = Supervisor::new(rt.spawner()).expect("supervisor");
373        let outcome = sup
374            .start_child(ChildSpec::new("main", 0).restart(RestartPolicy::OnFailure))
375            .expect("start_child")
376            .join();
377        wait_until(|| sup.live_children() == 0);
378        let spawned = rt.metrics().processes_spawned;
379        sup.shutdown();
380        rt.shutdown();
381        assert!(matches!(outcome, FlowOutcome::Completed(_)));
382        assert_eq!(spawned, 1);
383    }
384
385    #[test]
386    fn on_failure_restarts_until_intensity() {
387        let rt = tiny_runtime(trap_chunk());
388        let sup = Supervisor::with_config(
389            rt.spawner(),
390            SupervisorConfig {
391                max_restarts: 2,
392                max_period: Duration::from_secs(5),
393            },
394        )
395        .expect("supervisor");
396        let _first = sup
397            .start_child(ChildSpec::new("boom", 0).restart(RestartPolicy::OnFailure))
398            .expect("start_child");
399        wait_until(|| sup.intensity_exceeded() && rt.metrics().processes_failed >= 3);
400        let spawned = rt.metrics().processes_spawned;
401        let failed = rt.metrics().processes_failed;
402        sup.shutdown();
403        rt.shutdown();
404        // initial start + 2 restarts, then intensity refuses the 3rd restart
405        assert_eq!(spawned, 3);
406        assert_eq!(failed, 3);
407    }
408
409    #[test]
410    fn always_restarts_a_clean_exit_until_intensity() {
411        let rt = tiny_runtime(ok_chunk());
412        let sup = Supervisor::with_config(
413            rt.spawner(),
414            SupervisorConfig {
415                max_restarts: 2,
416                max_period: Duration::from_secs(5),
417            },
418        )
419        .expect("supervisor");
420        let _ = sup
421            .start_child(ChildSpec::new("main", 0).restart(RestartPolicy::Always))
422            .expect("start_child");
423        wait_until(|| sup.intensity_exceeded() && rt.metrics().processes_completed >= 3);
424        let spawned = rt.metrics().processes_spawned;
425        sup.shutdown();
426        rt.shutdown();
427        assert_eq!(spawned, 3);
428    }
429
430    #[test]
431    fn policy_table() {
432        let ok = FlowOutcome::Completed(Value::Unit);
433        let fail = FlowOutcome::Failed("boom".into());
434        assert!(should_restart(RestartPolicy::Always, &ok));
435        assert!(should_restart(RestartPolicy::Always, &fail));
436        assert!(!should_restart(RestartPolicy::OnFailure, &ok));
437        assert!(should_restart(RestartPolicy::OnFailure, &fail));
438        assert!(!should_restart(RestartPolicy::Never, &ok));
439        assert!(!should_restart(RestartPolicy::Never, &fail));
440    }
441}