Skip to main content

byteflow/scheduler/
error.rs

1//! Infrastructure failures of the scheduler (category C in the error model).
2//!
3//! # Failure taxonomy (Byteflow)
4//!
5//! | Kind | Example | Surface |
6//! |------|---------|---------|
7//! | A — user / API | `spawn` bad function index | `Result<_, SpawnError>` |
8//! | B — flow | `HwError`, native fault | `FlowOutcome::Failed` → Supervisor |
9//! | C — infrastructure | mutex poison, dead worker | [`RuntimeError`] + fail-closed |
10//! | D — invariant | empty frame stack while running | types / `debug_assert` — not `unwrap` |
11//!
12//! **Panic is a runtime bug, not an error-handling mechanism.** Device faults
13//! kill actors; scheduler faults are explicit and fail-closed.
14
15use std::fmt;
16use std::sync::atomic::{AtomicU64, Ordering};
17
18/// Count of infrastructure faults observed since flow start (Relaxed).
19static FAULTS: AtomicU64 = AtomicU64::new(0);
20
21/// Scheduler / host infrastructure error — not a bytecode flow fault.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum RuntimeError {
24    /// A `Mutex` was poisoned: another thread panicked while holding it.
25    /// Shared tables may be inconsistent; callers must not continue using them.
26    PoisonedLock(&'static str),
27    /// A flow was destroyed before it produced a [`crate::FlowOutcome`], so
28    /// the completion value its handle was waiting for will never arrive.
29    ///
30    /// Reachable when the runtime shuts down while a flow is suspended (in
31    /// the timer, in a worker deque, or parked in its mailbox): those flows
32    /// are dropped without reaching `finish`. The alternative to reporting
33    /// this is worse — a `join()` that blocks the embedder's thread forever
34    /// with no way to tell "still running" from "never will".
35    Abandoned(&'static str),
36    /// The outcome was already collected by an earlier non-consuming
37    /// `try_join` / `join_timeout`, so there is nothing left to hand out.
38    ///
39    /// Caller misuse rather than an infrastructure failure (kind A in the
40    /// table above), reported here because it shares `join`'s return type.
41    /// It exists so a second poll cannot be answered with `Abandoned`, which
42    /// would blame the runtime for destroying a flow that in fact completed
43    /// normally and was already observed.
44    AlreadyCollected(&'static str),
45    /// OS CSPRNG failed while minting a capability.
46    EntropyFailed,
47    /// CSPRNG produced colliding ids beyond the retry budget (should not happen).
48    CapIdCollision,
49    /// `finalize_flow` was asked to tear down the reserved host FlowId.
50    CannotFinalizeHostFlow,
51}
52
53impl fmt::Display for RuntimeError {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        match self {
56            RuntimeError::PoisonedLock(where_) => {
57                write!(f, "runtime mutex poisoned at {where_}")
58            }
59            RuntimeError::Abandoned(where_) => {
60                write!(
61                    f,
62                    "{where_}: flow was destroyed before producing an outcome"
63                )
64            }
65            RuntimeError::AlreadyCollected(where_) => {
66                write!(f, "{where_}: outcome was already collected")
67            }
68            RuntimeError::EntropyFailed => {
69                write!(f, "capability CSPRNG unavailable")
70            }
71            RuntimeError::CapIdCollision => {
72                write!(f, "could not allocate a unique capability id")
73            }
74            RuntimeError::CannotFinalizeHostFlow => {
75                write!(f, "cannot finalize reserved host flow")
76            }
77        }
78    }
79}
80
81impl std::error::Error for RuntimeError {}
82
83/// User-facing lifecycle / naming errors (category A).
84///
85/// Distinct from [`RuntimeError`] (infrastructure / poison). Host
86/// `monitor` / `link` / `register_name` return this so a dead FlowId or
87/// a duplicate name is not reported as a poisoned mutex.
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub enum LifecycleError {
90    NoSuchFlow(super::process::FlowId),
91    InvalidCapability,
92    InvalidMonitor,
93    InvalidLink,
94    NotOwner,
95    AlreadyRegistered,
96    AlreadyLinked,
97    EmptyName,
98    SelfRelation,
99    Unavailable,
100}
101
102impl fmt::Display for LifecycleError {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        match self {
105            Self::NoSuchFlow(id) => write!(f, "no live flow {id}"),
106            Self::InvalidCapability => write!(f, "unknown or revoked capability"),
107            Self::InvalidMonitor => write!(f, "unknown monitor"),
108            Self::InvalidLink => write!(f, "unknown link"),
109            Self::NotOwner => write!(f, "caller does not own this relation"),
110            Self::AlreadyRegistered => write!(f, "registry name already taken"),
111            Self::AlreadyLinked => write!(f, "flows are already linked"),
112            Self::EmptyName => write!(f, "registry name must be non-empty"),
113            Self::SelfRelation => write!(f, "cannot link or monitor a flow to itself"),
114            Self::Unavailable => write!(f, "runtime table unavailable (poisoned lock)"),
115        }
116    }
117}
118
119impl std::error::Error for LifecycleError {}
120
121/// Record an infrastructure fault (stderr + counter). Does not panic.
122#[cold]
123pub fn report_fault(err: RuntimeError) {
124    FAULTS.fetch_add(1, Ordering::Relaxed);
125    eprintln!("byteflow: {err} — fail-closed");
126}
127
128/// How many [`report_fault`] calls have been made (tests / diagnostics).
129pub fn fault_count() -> u64 {
130    FAULTS.load(Ordering::Relaxed)
131}
132
133/// User-facing spawn / load errors (category A in the taxonomy above).
134///
135/// These used to be `.expect(...)` panics on `Runtime::new` / `spawn` /
136/// OS thread creation. Panic is reserved for *bugs*; a bad function index
137/// or a refused `thread::spawn` is an embedder-visible failure and must
138/// surface as `Result` so the host can recover without taking workers down.
139#[derive(Debug, Clone, PartialEq, Eq)]
140pub enum SpawnError {
141    /// `function` index is outside the runtime chunk's function table.
142    BadFunction { index: u32, table_size: u32 },
143    /// Chunk failed verification before the runtime could start.
144    VerifyFailed(crate::bytecode::VerifyError),
145    /// Spawn arguments contained an unknown or unusable capability.
146    InvalidCapability,
147    /// Scheduler table poisoned (fail closed).
148    Unavailable,
149    /// Live flow count would exceed [`crate::RuntimeConfig::max_flows`].
150    FlowLimit { current: usize, max: u32 },
151    /// Bytecode spawn failed the quota / SPAWN-right / attenuation check.
152    SpawnDenied(String),
153    /// [`crate::ChildSpec::name`] is already in the runtime registry.
154    NameTaken { name: String },
155    /// OS refused to create a worker / timer / supervisor thread.
156    ThreadSpawnFailed(String),
157    /// `Vm::new` failed for a reason other than a bad function index
158    /// (mapped from [`crate::Fault`] via `From`).
159    VmInit(String),
160}
161
162impl fmt::Display for SpawnError {
163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164        match self {
165            SpawnError::BadFunction { index, table_size } => {
166                write!(
167                    f,
168                    "spawn: function index {index} out of range (table size {table_size})"
169                )
170            }
171            SpawnError::VerifyFailed(err) => write!(f, "chunk verification failed: {err}"),
172            SpawnError::InvalidCapability => {
173                write!(f, "spawn: argument capability is unknown or not held")
174            }
175            SpawnError::Unavailable => write!(f, "spawn: runtime table unavailable"),
176            SpawnError::FlowLimit { current, max } => {
177                write!(f, "spawn: live flow limit reached ({current}/{max})")
178            }
179            SpawnError::SpawnDenied(msg) => write!(f, "spawn: {msg}"),
180            SpawnError::NameTaken { name } => {
181                write!(f, "spawn: registry name {name:?} already taken")
182            }
183            SpawnError::ThreadSpawnFailed(msg) => {
184                write!(f, "failed to spawn runtime thread: {msg}")
185            }
186            SpawnError::VmInit(msg) => write!(f, "vm init failed: {msg}"),
187        }
188    }
189}
190
191impl std::error::Error for SpawnError {}
192
193impl From<crate::vm::Fault> for SpawnError {
194    fn from(fault: crate::vm::Fault) -> Self {
195        match fault {
196            crate::vm::Fault::BadFunction { index, table_size } => {
197                SpawnError::BadFunction { index, table_size }
198            }
199            other => SpawnError::VmInit(other.to_string()),
200        }
201    }
202}