Skip to main content

byteflow/scheduler/
runtime.rs

1use std::sync::atomic::{AtomicBool, Ordering};
2use std::sync::{Arc, Condvar, Mutex};
3use std::thread::JoinHandle;
4
5use crate::bytecode::{Chunk, Value};
6use crate::vm::{NativeTable, Vm};
7use crossbeam_deque::{Injector, Stealer, Worker as LocalDeque};
8
9use super::directory::Directory;
10use super::error::SpawnError;
11use super::handle::FlowHandle;
12use super::mailbox::{Delivery, Mailbox, MailboxConfig, MailboxFullReason};
13use super::metrics::{RuntimeMetrics, RuntimeMetricsSnapshot};
14use super::process::{Flow, FlowId, RestartPolicy};
15use super::supervisor::SupervisorLink;
16use super::timer::TimerWheel;
17use super::worker;
18
19/// Default instruction budget per scheduling turn (design notes §10).
20/// Chosen as a middle ground: large enough that the per-yield bookkeeping
21/// cost is amortized over meaningful work, small enough that a
22/// pathological `loop {}` in one flow can't visibly stall the others —
23/// at 10k simple instructions/turn and even a conservative tens-of-millions
24/// of instructions/sec per core, worst-case added latency for a sibling
25/// flow is sub-millisecond.
26pub const DEFAULT_QUANTUM: u32 = 10_000;
27
28/// Tunables for [`Runtime::new`]. Everything has a sensible default via
29/// [`RuntimeConfig::default`] so the common case is `Runtime::new(chunk)`.
30#[derive(Clone, Debug)]
31pub struct RuntimeConfig {
32    /// Number of worker OS threads. Defaults to the number of logical CPUs
33    /// — one worker per core is the right starting point for a CPU-bound
34    /// M:N scheduler; embedders running alongside other CPU-heavy work on
35    /// the same machine may want fewer.
36    pub workers: usize,
37    /// Instructions a flow runs before being preempted back to the
38    /// scheduler even if it never hits `Yield`.
39    pub quantum: u32,
40    /// Memory + overflow contract applied to **every** flow mailbox
41    /// spawned by this runtime (bytecode `Spawn` and host `spawn`).
42    /// See [`MailboxConfig`] / `docs/mailbox.md`.
43    pub mailbox: MailboxConfig,
44    /// Hard cap on concurrently live flows (`0` = unlimited).
45    /// Checked on every host and bytecode `spawn`.
46    pub max_flows: u32,
47    /// Trace JIT settings (`feature = "jit"`). Ignored when the feature is off.
48    #[cfg(feature = "jit")]
49    pub jit: JitConfig,
50}
51
52/// Trace JIT toggles for [`RuntimeConfig`] (`feature = "jit"`).
53#[cfg(feature = "jit")]
54#[derive(Clone, Debug)]
55pub struct JitConfig {
56    /// When true, workers attempt compiled traces before interpreting.
57    pub enabled: bool,
58    /// How many times a `(function, pc)` pair must run before compilation.
59    pub hot_threshold: u32,
60}
61
62#[cfg(feature = "jit")]
63impl Default for JitConfig {
64    fn default() -> Self {
65        JitConfig {
66            enabled: false,
67            hot_threshold: crate::jit::HOT_THRESHOLD,
68        }
69    }
70}
71
72impl Default for RuntimeConfig {
73    fn default() -> Self {
74        RuntimeConfig {
75            workers: num_cpus::get().max(1),
76            quantum: DEFAULT_QUANTUM,
77            mailbox: MailboxConfig::DEFAULT,
78            max_flows: 0,
79            #[cfg(feature = "jit")]
80            jit: JitConfig::default(),
81        }
82    }
83}
84
85/// State shared by every worker thread and the timer thread. Everything in
86/// here is either internally synchronized (`Injector`, `Directory`,
87/// `CapTable`, `TimerWheel`, the `RuntimeMetrics` atomics) or immutable after
88/// construction (`stealers`, `quantum`) — there is no top-level lock
89/// covering the whole runtime, by design: a global lock is exactly what an
90/// M:N scheduler exists to avoid.
91pub struct Shared {
92    pub(crate) injector: Injector<Box<Flow>>,
93    pub(crate) stealers: Vec<Stealer<Box<Flow>>>,
94    /// FlowId → mailbox (delivery after Cap resolution).
95    pub(crate) directory: Directory,
96    /// CapId → { FlowId, rights } (bytecode Send/Ask addressing — FlowCap).
97    pub(crate) caps: super::capability::CapTable,
98    pub(crate) timer: Arc<TimerWheel>,
99    pub(crate) notify: (Mutex<()>, Condvar),
100    pub(crate) metrics: RuntimeMetrics,
101    pub(crate) shutdown: AtomicBool,
102    pub(crate) quantum: u32,
103    pub(crate) mailbox: MailboxConfig,
104    pub(crate) max_flows: u32,
105    pub(crate) monitors: super::monitor::MonitorStore,
106    pub(crate) links: super::link::LinkStore,
107    pub(crate) registry: super::registry::RegistryStore,
108    pub(crate) kill_signals: super::finalize::KillSignals,
109    pub(crate) waiting_send_at: super::finalize::WaitingSendIndex,
110    pub(crate) ask_waits: super::finalize::AskWaitIndex,
111    /// Shared trace JIT state (`feature = "jit"`).
112    #[cfg(feature = "jit")]
113    pub(crate) jit: Option<std::sync::Arc<crate::jit::JitRuntime>>,
114}
115
116/// A running Byteflow runtime: worker pool + timer thread over one shared
117/// [`Chunk`].
118///
119/// Owns M:N scheduling for **flows** (spawn, yield, sleep, mailboxes,
120/// FlowCap resolution, supervised restarts). Optional trace JIT when built
121/// with `feature = "jit"` and enabled in [`RuntimeConfig::jit`].
122///
123/// Construct with [`Runtime::new`] (no natives) or
124/// [`Runtime::with_natives`] when the chunk uses `CallNative` /
125/// [`crate::std_native_table`].
126pub struct Runtime {
127    shared: Arc<Shared>,
128    chunk: Arc<Chunk>,
129    natives: Arc<NativeTable>,
130    workers: Vec<JoinHandle<()>>,
131    timer_thread: Option<JoinHandle<()>>,
132}
133
134impl Runtime {
135    /// Convenience constructor for chunks that never call out through
136    /// `Opcode::CallNative`. Equivalent to
137    /// `Runtime::with_natives(chunk, NativeTable::empty())`.
138    ///
139    /// Returns [`SpawnError`] instead of panicking: verify failures and OS
140    /// thread-spawn refusals are category-A errors (see
141    /// [`docs::error_model`](crate::docs::error_model)).
142    pub fn new(chunk: Chunk) -> Result<Self, SpawnError> {
143        Self::with_config(chunk, RuntimeConfig::default())
144    }
145
146    /// Construct a runtime whose flows can call into `natives` via
147    /// `Opcode::CallNative` — the host FFI boundary.
148    pub fn with_natives(chunk: Chunk, natives: Arc<NativeTable>) -> Result<Self, SpawnError> {
149        Self::with_natives_and_config(chunk, natives, RuntimeConfig::default())
150    }
151
152    pub fn with_config(chunk: Chunk, config: RuntimeConfig) -> Result<Self, SpawnError> {
153        Self::with_natives_and_config(chunk, NativeTable::empty(), config)
154    }
155
156    /// Verify `chunk`, spawn the worker pool + timer thread, and return a
157    /// live [`Runtime`].
158    ///
159    /// Failures here mean the runtime was **never** started (no orphan
160    /// threads): either the bytecode is invalid
161    /// ([`SpawnError::VerifyFailed`]) or the OS refused a thread
162    /// ([`SpawnError::ThreadSpawnFailed`]).
163    pub fn with_natives_and_config(
164        chunk: Chunk,
165        natives: Arc<NativeTable>,
166        config: RuntimeConfig,
167    ) -> Result<Self, SpawnError> {
168        crate::bytecode::verify(&chunk).map_err(|e| SpawnError::VerifyFailed(e.to_string()))?;
169        let chunk = Arc::new(chunk);
170        let workers_n = config.workers.max(1);
171
172        let locals: Vec<LocalDeque<Box<Flow>>> =
173            (0..workers_n).map(|_| LocalDeque::new_fifo()).collect();
174        let stealers: Vec<Stealer<Box<Flow>>> = locals.iter().map(|l| l.stealer()).collect();
175
176        #[cfg(feature = "jit")]
177        let jit = if config.jit.enabled {
178            Some(super::jit::new_runtime(chunk.clone(), config.jit.hot_threshold))
179        } else {
180            None
181        };
182
183        let shared = Arc::new(Shared {
184            injector: Injector::new(),
185            stealers,
186            directory: Directory::new(),
187            caps: super::capability::CapTable::new(),
188            timer: TimerWheel::new(),
189            notify: (Mutex::new(()), Condvar::new()),
190            metrics: RuntimeMetrics::default(),
191            shutdown: AtomicBool::new(false),
192            quantum: config.quantum,
193            mailbox: config.mailbox,
194            max_flows: config.max_flows,
195            monitors: super::monitor::MonitorStore::new(),
196            links: super::link::LinkStore::new(),
197            registry: super::registry::RegistryStore::new(),
198            kill_signals: super::finalize::KillSignals::new(),
199            waiting_send_at: super::finalize::WaitingSendIndex::new(),
200            ask_waits: super::finalize::AskWaitIndex::new(),
201            #[cfg(feature = "jit")]
202            jit,
203        });
204
205        let mut workers = Vec::with_capacity(workers_n);
206        for local in locals {
207            let shared = shared.clone();
208            let handle = std::thread::Builder::new()
209                .name("byteflow-worker".into())
210                .spawn(move || worker::run_worker(shared, local))
211                .map_err(|e| SpawnError::ThreadSpawnFailed(e.to_string()))?;
212            workers.push(handle);
213        }
214
215        let shared_timer = shared.clone();
216        let timer_thread = std::thread::Builder::new()
217            .name("byteflow-timer".into())
218            .spawn(move || {
219                shared_timer
220                    .timer
221                    .clone()
222                    .drive(
223                        &shared_timer.injector,
224                        &shared_timer.notify,
225                        &shared_timer.ask_waits,
226                    )
227            })
228            .map_err(|e| SpawnError::ThreadSpawnFailed(e.to_string()))?;
229
230        Ok(Runtime {
231            shared,
232            chunk,
233            natives,
234            workers,
235            timer_thread: Some(timer_thread),
236        })
237    }
238
239    /// Spawn a top-level flow starting at `function` in this runtime's
240    /// chunk, returning a [`FlowHandle`] the caller can `.join()`.
241    ///
242    /// Returns [`SpawnError::BadFunction`] if `function` is out of range.
243    pub fn spawn(&self, function: u32, args: &[Value]) -> Result<FlowHandle, SpawnError> {
244        spawn_on(
245            &self.shared,
246            &self.chunk,
247            &self.natives,
248            function,
249            args,
250            RestartPolicy::Never,
251            None,
252        )
253    }
254
255    /// A cheap, `Send + Sync` handle that can spawn processes into this
256    /// runtime from any thread, independent of `Runtime`'s own lifetime
257    /// bookkeeping (worker `JoinHandle`s). Used by [`super::supervisor::Supervisor`].
258    pub fn spawner(&self) -> RuntimeSpawner {
259        RuntimeSpawner { shared: self.shared.clone(), chunk: self.chunk.clone(), natives: self.natives.clone() }
260    }
261
262    /// A [`super::supervisor::Supervisor`] bound to this runtime, ready to
263    /// take supervised children (design notes §15).
264    pub fn supervisor(&self) -> Result<super::supervisor::Supervisor, SpawnError> {
265        super::supervisor::Supervisor::new(self.spawner())
266    }
267
268    /// Look up a function by name in the runtime's chunk — convenience for
269    /// callers that built their chunk with [`crate::Program`]
270    /// and don't want to thread raw indices through their own code.
271    pub fn function_index(&self, name: &str) -> Option<u32> {
272        self.chunk.functions.iter().position(|f| f.name == name).map(|i| i as u32)
273    }
274
275    pub fn metrics(&self) -> RuntimeMetricsSnapshot {
276        self.shared.metrics.snapshot()
277    }
278
279    /// Number of flows currently registered in the directory — i.e.
280    /// alive (running, ready, sleeping, or waiting), not counting ones that
281    /// have already completed or failed.
282    pub fn live_flows(&self) -> usize {
283        self.shared.directory.len()
284    }
285
286    pub fn worker_count(&self) -> usize {
287        self.workers.len()
288    }
289
290    /// Deliver an **Atomic Hop** (`Value::Message`) to `target` from the
291    /// embedder (not from bytecode).
292    ///
293    /// # Host trust boundary
294    ///
295    /// This path takes a [`FlowId`] directly — **no Cap required**. The host
296    /// is trusted; bytecode must use `Value::Cap` via `Opcode::Send` /
297    /// `Ask`. Host-injected messages are not re-stamped (`sender` /
298    /// `reply_cap` stay as built). Bare scalars are rejected
299    /// ([`SendError::NotAHop`]).
300    pub fn send(&self, target: FlowId, message: Value) -> Result<(), SendError> {
301        if message.as_message().is_none() {
302            return Err(SendError::NotAHop {
303                got: message.type_name(),
304            });
305        }
306        let mailbox = match self.shared.directory.lookup(target) {
307            Ok(Some(m)) => m,
308            Ok(None) => return Err(SendError::NoSuchFlow(target)),
309            Err(e) => {
310                super::error::report_fault(e);
311                return Err(SendError::NoSuchFlow(target));
312            }
313        };
314        match mailbox.push(message.clone()) {
315            Ok(Ok(Delivery::Queued | Delivery::QueuedDropOldest | Delivery::DroppedNewest)) => {
316                Ok(())
317            }
318            Ok(Ok(Delivery::Handoff(mut flow))) => {
319                let _ = self.shared.ask_waits.remove_asker(flow.id);
320                if let Some(dest) = flow.last_receive_dest {
321                    let _ = flow.vm.resume_with(dest, message);
322                }
323                self.shared.injector.push(flow);
324                wake_workers(&self.shared);
325                Ok(())
326            }
327            Ok(Err(full)) => Err(SendError::MailboxFull {
328                flow: target,
329                reason: full.reason(),
330            }),
331            Err(e) => {
332                super::error::report_fault(e);
333                Err(SendError::NoSuchFlow(target))
334            }
335        }
336    }
337
338    fn require_live(&self, id: FlowId) -> Result<(), super::error::LifecycleError> {
339        match self.shared.directory.lookup(id) {
340            Ok(Some(_)) => Ok(()),
341            Ok(None) => Err(super::error::LifecycleError::NoSuchFlow(id)),
342            Err(e) => Err(self.unavailable(e)),
343        }
344    }
345
346    fn unavailable(&self, err: super::error::RuntimeError) -> super::error::LifecycleError {
347        super::error::report_fault(err);
348        super::error::LifecycleError::Unavailable
349    }
350
351    /// Mint a SEND|ASK Cap for a live flow (host equivalent of `SelfPid`).
352    pub fn mint_cap(&self, flow: FlowId) -> Result<super::capability::CapId, super::error::LifecycleError> {
353        self.require_live(flow)?;
354        self.shared
355            .caps
356            .mint(flow, super::capability::CapRights::SEND_ASK)
357            .map_err(|e| self.unavailable(e))
358    }
359
360    /// Watch `target`; when it exits, `owner` receives a [`crate::TAG_SYS_DOWN`] hop.
361    ///
362    /// Both flows must be live. `owner == target` is [`LifecycleError::SelfRelation`].
363    pub fn monitor(
364        &self,
365        owner: FlowId,
366        target: FlowId,
367    ) -> Result<super::monitor::MonitorRef, super::error::LifecycleError> {
368        if owner == target {
369            return Err(super::error::LifecycleError::SelfRelation);
370        }
371        self.require_live(owner)?;
372        self.require_live(target)?;
373        let mon = self
374            .shared
375            .monitors
376            .create(owner, target)
377            .map_err(|e| self.unavailable(e))?;
378        // Target may have finalized between the live check and insert.
379        // Synthesize DOWN and drop the now-useless relation (owner still live).
380        if self.require_live(target).is_err() {
381            let _ = self.shared.monitors.remove_owned(owner, mon);
382            super::finalize::deliver_down(
383                &self.shared,
384                super::monitor::DownEvent {
385                    monitor: mon,
386                    owner,
387                    target,
388                    reason: super::monitor::FlowExitReason::Fault,
389                },
390            );
391        }
392        Ok(mon)
393    }
394
395    /// Drop `monitor` if `owner` still owns it.
396    pub fn demonitor(
397        &self,
398        owner: FlowId,
399        monitor: super::monitor::MonitorRef,
400    ) -> Result<(), super::error::LifecycleError> {
401        self.require_live(owner)?;
402        match self.shared.monitors.remove_owned(owner, monitor) {
403            Ok(inner) => inner,
404            Err(e) => Err(self.unavailable(e)),
405        }
406    }
407
408    /// Bidirectional link. Abnormal exit of either side kills the peer.
409    pub fn link(
410        &self,
411        a: FlowId,
412        b: FlowId,
413    ) -> Result<super::link::LinkId, super::error::LifecycleError> {
414        if a == b {
415            return Err(super::error::LifecycleError::SelfRelation);
416        }
417        self.require_live(a)?;
418        self.require_live(b)?;
419        match self.shared.links.link(a, b) {
420            Ok(inner) => inner,
421            Err(e) => Err(self.unavailable(e)),
422        }
423    }
424
425    /// Drop `link` if `owner` is one of the endpoints.
426    pub fn unlink(
427        &self,
428        owner: FlowId,
429        link: super::link::LinkId,
430    ) -> Result<(), super::error::LifecycleError> {
431        self.require_live(owner)?;
432        match self.shared.links.unlink_owned(owner, link) {
433            Ok(inner) => inner,
434            Err(e) => Err(self.unavailable(e)),
435        }
436    }
437
438    /// Bind `name` to a live Cap (address). Names are swept when that flow exits.
439    pub fn register_name(
440        &self,
441        name: &str,
442        cap: super::capability::CapId,
443    ) -> Result<(), super::error::LifecycleError> {
444        let entry = match self.shared.caps.resolve(cap) {
445            Ok(Some(e)) => e,
446            Ok(None) => return Err(super::error::LifecycleError::InvalidCapability),
447            Err(e) => return Err(self.unavailable(e)),
448        };
449        self.require_live(entry.flow)?;
450        match self.shared.registry.register(
451            super::registry::RegistryName::from(name),
452            cap,
453            entry.flow,
454        ) {
455            Ok(inner) => inner,
456            Err(e) => Err(self.unavailable(e)),
457        }
458    }
459
460    /// Look up a registered Cap, or `None` if the name is free / was swept.
461    pub fn whereis(&self, name: &str) -> Result<Option<super::capability::CapId>, super::error::LifecycleError> {
462        self.shared
463            .registry
464            .whereis(name)
465            .map_err(|e| self.unavailable(e))
466    }
467
468    /// Cooperative abort. Parked flows finalize immediately; a running flow
469    /// dies at the next quantum with [`super::monitor::FlowExitReason::Killed`].
470    pub fn kill(&self, id: FlowId) -> Result<(), super::error::LifecycleError> {
471        self.require_live(id)?;
472        super::finalize::request_kill(&self.shared, id, super::monitor::FlowExitReason::Killed);
473        Ok(())
474    }
475
476    /// Remove a name without waiting for the flow to exit. `false` if unknown.
477    pub fn unregister_name(&self, name: &str) -> Result<bool, super::error::LifecycleError> {
478        self.shared
479            .registry
480            .unregister(name)
481            .map_err(|e| self.unavailable(e))
482    }
483
484    /// Replace the image used by **new host** [`Self::spawn`] calls and
485    /// drop JIT traces. Live flows and bytecode `Spawn` keep the parent's
486    /// existing `Vm` chunk.
487    pub fn reload_chunk(&mut self, chunk: Chunk) -> Result<(), SpawnError> {
488        crate::bytecode::verify(&chunk).map_err(|e| SpawnError::VerifyFailed(e.to_string()))?;
489        let chunk = Arc::new(chunk);
490        self.chunk = chunk.clone();
491        #[cfg(feature = "jit")]
492        if let Some(jit) = &self.shared.jit {
493            jit.reload(chunk);
494        }
495        Ok(())
496    }
497
498    /// Stop accepting new scheduling work and join every worker + the timer
499    /// thread. Processes that are mid-quantum are allowed to reach their
500    /// next natural suspension point; this does **not** forcibly abort
501    /// running bytecode (there is no safe way to do that to an OS thread
502    /// mid-instruction — see design notes §11 on why preemption here is
503    /// cooperative/budgeted rather than signal-based).
504    pub fn shutdown(mut self) {
505        self.shared.shutdown.store(true, Ordering::Release);
506        self.shared.timer.shutdown();
507        {
508            let (lock, cvar) = &self.shared.notify;
509            match super::sync_lock::lock(lock, "Runtime::shutdown") {
510                Ok(_g) => cvar.notify_all(),
511                Err(e) => super::error::report_fault(e),
512            }
513        }
514        for w in self.workers.drain(..) {
515            let _ = w.join();
516        }
517        if let Some(t) = self.timer_thread.take() {
518            let _ = t.join();
519        }
520    }
521}
522
523/// A convenience Pid constructor for embedders that stored a raw `u64`
524/// (e.g. round-tripped through `Value::Pid`) and need a [`FlowId`] to
525/// call APIs that take one.
526pub fn flow_id_from_u64(raw: u64) -> FlowId {
527    FlowId(raw)
528}
529
530/// Why [`Runtime::send`] could not deliver a hop.
531#[derive(Debug, Clone, PartialEq, Eq)]
532pub enum SendError {
533    NoSuchFlow(FlowId),
534    /// Atomic Hop rule: only [`crate::Value::Message`] may cross `Send`.
535    NotAHop { got: &'static str },
536    /// Target inbox is at one of its logical bounds
537    /// ([`OverflowPolicy::Reject`](crate::OverflowPolicy::Reject)). `reason` says which — see
538    /// [`MailboxFullReason`].
539    MailboxFull {
540        flow: FlowId,
541        reason: MailboxFullReason,
542    },
543}
544
545impl std::fmt::Display for SendError {
546    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
547        match self {
548            SendError::NoSuchFlow(id) => write!(f, "no live flow {id}"),
549            SendError::NotAHop { got } => {
550                write!(f, "atomic hop requires Value::Message, got {got}")
551            }
552            SendError::MailboxFull { flow, reason } => {
553                write!(f, "mailbox full for {flow} ({reason})")
554            }
555        }
556    }
557}
558
559impl std::error::Error for SendError {}
560
561/// Shared machinery behind `Runtime::spawn` and `RuntimeSpawner::spawn`
562/// (and, transitively, `Supervisor`): build a fresh `Flow` (VM +
563/// mailbox + completion channel), register it in the directory, and push
564/// it onto the global injector for any worker to pick up.
565///
566/// Returns [`SpawnError`] on bad function index / VM init / directory
567/// poison — never panics. Bytecode `Opcode::Spawn` that fails here turns
568/// into `FlowOutcome::Failed` for the *parent* (see `worker`).
569pub(crate) fn spawn_on(
570    shared: &Arc<Shared>,
571    chunk: &Arc<Chunk>,
572    natives: &Arc<NativeTable>,
573    function: u32,
574    args: &[Value],
575    restart_policy: RestartPolicy,
576    supervisor: Option<SupervisorLink>,
577) -> Result<FlowHandle, SpawnError> {
578    if shared.max_flows > 0 {
579        let current = shared.directory.len();
580        if current >= shared.max_flows as usize {
581            return Err(SpawnError::FlowLimit {
582                current,
583                max: shared.max_flows,
584            });
585        }
586    }
587    let id = super::process::next_flow_id();
588    let vm = Vm::new(chunk.clone(), natives.clone(), function, args)?;
589    let mailbox = Arc::new(Mailbox::with_config(shared.mailbox));
590    if let Err(e) = shared.directory.register(id, mailbox.clone()) {
591        super::error::report_fault(e);
592        return Err(SpawnError::VmInit(
593            "directory register failed (poisoned lock)".into(),
594        ));
595    }
596    let (tx, rx) = super::oneshot::channel();
597    let mut flow = Box::new(Flow::new(id, vm, mailbox, restart_policy, tx));
598    flow.supervisor = supervisor;
599    RuntimeMetrics::inc(&shared.metrics.processes_spawned);
600    shared.injector.push(flow);
601    wake_workers(shared);
602    Ok(FlowHandle { id, receiver: rx })
603}
604
605pub(crate) fn wake_workers(shared: &Shared) {
606    let (lock, cvar) = &shared.notify;
607    match super::sync_lock::lock(lock, "wake_workers") {
608        Ok(_g) => cvar.notify_one(),
609        Err(e) => super::error::report_fault(e),
610    }
611}
612
613/// A `Send + Sync`, freely cloneable capability to spawn processes into a
614/// [`Runtime`], detached from the `Runtime` value itself. Exists because
615/// [`super::supervisor::Supervisor`] needs to respawn processes from a
616/// background monitor thread whose lifetime isn't tied to the `Runtime`
617/// object's own (which owns non-`Sync` `JoinHandle`s for its workers).
618#[derive(Clone)]
619pub struct RuntimeSpawner {
620    pub(crate) shared: Arc<Shared>,
621    pub(crate) chunk: Arc<Chunk>,
622    pub(crate) natives: Arc<NativeTable>,
623}
624
625impl RuntimeSpawner {
626    pub fn spawn(
627        &self,
628        function: u32,
629        args: &[Value],
630        restart_policy: RestartPolicy,
631    ) -> Result<FlowHandle, SpawnError> {
632        spawn_on(
633            &self.shared,
634            &self.chunk,
635            &self.natives,
636            function,
637            args,
638            restart_policy,
639            None,
640        )
641    }
642
643    pub(crate) fn spawn_linked(
644        &self,
645        function: u32,
646        args: &[Value],
647        restart_policy: RestartPolicy,
648        supervisor: SupervisorLink,
649    ) -> Result<FlowHandle, SpawnError> {
650        spawn_on(
651            &self.shared,
652            &self.chunk,
653            &self.natives,
654            function,
655            args,
656            restart_policy,
657            Some(supervisor),
658        )
659    }
660
661    pub fn metrics(&self) -> RuntimeMetricsSnapshot {
662        self.shared.metrics.snapshot()
663    }
664
665    pub(crate) fn request_kill(&self, id: FlowId, reason: super::monitor::FlowExitReason) {
666        super::finalize::request_kill(&self.shared, id, reason);
667    }
668}
669
670#[cfg(test)]
671mod tests {
672    use super::*;
673    use crate::bytecode::{builder::ChunkBuilder, Opcode, Value};
674    use crate::scheduler::FlowOutcome;
675    use std::time::Duration;
676
677    fn add_chunk() -> Chunk {
678        let mut b = ChunkBuilder::new("test");
679        b.begin_function("main", 0, 2);
680        b.emit_load_imm(0, 41);
681        b.emit_load_imm(1, 1);
682        b.emit_binop(Opcode::Add, 0, 0, 1);
683        b.emit_return(0);
684        b.finish()
685    }
686
687    /// Sleeps `millis` inside the flow, then returns 7. The sleep is what
688    /// makes "still running" an observable state from the host thread.
689    fn sleep_then_return_chunk(millis: i32) -> Chunk {
690        let mut b = ChunkBuilder::new("test");
691        b.begin_function("main", 0, 2);
692        b.emit_load_imm(0, millis);
693        b.emit_sleep(0);
694        b.emit_load_imm(0, 7);
695        b.emit_return(0);
696        b.finish()
697    }
698
699    /// The host thread must be able to ask "done yet?" and to wait under a
700    /// bound *it* chooses, instead of surrendering itself to `join` for
701    /// however long the bytecode decides to take.
702    #[test]
703    fn polling_and_bounded_waits_never_commit_the_host_thread() -> Result<(), Box<dyn std::error::Error>>
704    {
705        const FLOW_SLEEP: i32 = 150;
706        let rt = Runtime::with_config(
707            sleep_then_return_chunk(FLOW_SLEEP),
708            RuntimeConfig {
709                workers: 1,
710                quantum: 1_000,
711                mailbox: MailboxConfig::DEFAULT,
712                ..Default::default()
713            },
714        )?;
715        let handle = rt.spawn(0, &[])?;
716
717        // The flow cannot possibly be finished yet: it has to be picked up
718        // and then sleep. A poll must say so without waiting.
719        if let Some(outcome) = handle.try_join() {
720            rt.shutdown();
721            return Err(format!("try_join answered too early: {outcome:?}").into());
722        }
723
724        // A bound well below the flow's sleep must expire and hand control
725        // back, not block until the flow happens to finish.
726        if let Some(outcome) = handle.join_timeout(Duration::from_millis(20)) {
727            rt.shutdown();
728            return Err(format!("join_timeout answered too early: {outcome:?}").into());
729        }
730
731        // A generous bound collects the real outcome through the same
732        // (non-consuming) handle.
733        let outcome = handle.join_timeout(Duration::from_secs(10));
734        rt.shutdown();
735        match outcome {
736            Some(FlowOutcome::Completed(Value::Int(7))) => Ok(()),
737            other => Err(format!("unexpected outcome: {other:?}").into()),
738        }
739    }
740
741    #[test]
742    fn spawn_and_join_add() -> Result<(), Box<dyn std::error::Error>> {
743        let rt = Runtime::with_config(
744            add_chunk(),
745            RuntimeConfig {
746                workers: 1,
747                quantum: 1_000,
748                mailbox: MailboxConfig::DEFAULT,
749                ..Default::default()
750            },
751        )?;
752        let outcome = rt.spawn(0, &[])?.join();
753        rt.shutdown();
754        match outcome {
755            FlowOutcome::Completed(Value::Int(42)) => Ok(()),
756            other => Err(format!("unexpected outcome: {other:?}").into()),
757        }
758    }
759
760    fn receive_forever_chunk() -> Chunk {
761        let mut b = ChunkBuilder::new("recv");
762        b.begin_function("main", 0, 1);
763        b.emit_receive(0);
764        b.emit_return(0);
765        b.finish()
766    }
767
768    #[test]
769    fn kill_parked_flow_joins_failed() -> Result<(), Box<dyn std::error::Error>> {
770        let rt = Runtime::with_config(
771            receive_forever_chunk(),
772            RuntimeConfig {
773                workers: 1,
774                quantum: 1_000,
775                mailbox: MailboxConfig::DEFAULT,
776                ..Default::default()
777            },
778        )?;
779        let handle = rt.spawn(0, &[])?;
780        rt.kill(handle.id())?;
781        let outcome = handle.join();
782        rt.shutdown();
783        assert!(
784            matches!(outcome, FlowOutcome::Failed(_)),
785            "kill must fail the joiner, got {outcome:?}"
786        );
787        Ok(())
788    }
789
790    #[test]
791    fn max_flows_rejects_extra_spawn() -> Result<(), Box<dyn std::error::Error>> {
792        let rt = Runtime::with_config(
793            receive_forever_chunk(),
794            RuntimeConfig {
795                workers: 1,
796                quantum: 1_000,
797                mailbox: MailboxConfig::DEFAULT,
798                max_flows: 1,
799                ..Default::default()
800            },
801        )?;
802        let first = rt.spawn(0, &[])?;
803        let second = rt.spawn(0, &[]);
804        rt.kill(first.id())?;
805        let _ = first.join();
806        rt.shutdown();
807        match second {
808            Err(SpawnError::FlowLimit { current, max }) => {
809                assert_eq!(current, 1);
810                assert_eq!(max, 1);
811                Ok(())
812            }
813            other => Err(format!(
814                "expected FlowLimit, got {}",
815                match &other {
816                    Ok(_) => "Ok(handle)".into(),
817                    Err(e) => format!("Err({e})"),
818                }
819            )
820            .into()),
821        }
822    }
823
824    #[cfg(feature = "jit")]
825    #[test]
826    fn runtime_with_jit_enabled_completes_add() -> Result<(), Box<dyn std::error::Error>> {
827        use crate::JitConfig;
828
829        let rt = Runtime::with_config(
830            add_chunk(),
831            RuntimeConfig {
832                workers: 1,
833                quantum: 1_000,
834                mailbox: MailboxConfig::DEFAULT,
835                jit: JitConfig {
836                    enabled: true,
837                    hot_threshold: 1,
838                },
839                ..Default::default()
840            },
841        )?;
842        let outcome = rt.spawn(0, &[])?.join();
843        rt.shutdown();
844        match outcome {
845            FlowOutcome::Completed(Value::Int(42)) => Ok(()),
846            other => Err(format!("unexpected outcome: {other:?}").into()),
847        }
848    }
849}