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}
50
51impl fmt::Display for RuntimeError {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 match self {
54 RuntimeError::PoisonedLock(where_) => {
55 write!(f, "runtime mutex poisoned at {where_}")
56 }
57 RuntimeError::Abandoned(where_) => {
58 write!(
59 f,
60 "{where_}: flow was destroyed before producing an outcome"
61 )
62 }
63 RuntimeError::AlreadyCollected(where_) => {
64 write!(f, "{where_}: outcome was already collected")
65 }
66 RuntimeError::EntropyFailed => {
67 write!(f, "capability CSPRNG unavailable")
68 }
69 RuntimeError::CapIdCollision => {
70 write!(f, "could not allocate a unique capability id")
71 }
72 }
73 }
74}
75
76impl std::error::Error for RuntimeError {}
77
78/// User-facing lifecycle / naming errors (category A).
79///
80/// Distinct from [`RuntimeError`] (infrastructure / poison). Host
81/// `monitor` / `link` / `register_name` return this so a dead FlowId or
82/// a duplicate name is not reported as a poisoned mutex.
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub enum LifecycleError {
85 NoSuchFlow(super::process::FlowId),
86 InvalidCapability,
87 InvalidMonitor,
88 InvalidLink,
89 NotOwner,
90 AlreadyRegistered,
91 AlreadyLinked,
92 EmptyName,
93 SelfRelation,
94 Unavailable,
95}
96
97impl fmt::Display for LifecycleError {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 match self {
100 Self::NoSuchFlow(id) => write!(f, "no live flow {id}"),
101 Self::InvalidCapability => write!(f, "unknown or revoked capability"),
102 Self::InvalidMonitor => write!(f, "unknown monitor"),
103 Self::InvalidLink => write!(f, "unknown link"),
104 Self::NotOwner => write!(f, "caller does not own this relation"),
105 Self::AlreadyRegistered => write!(f, "registry name already taken"),
106 Self::AlreadyLinked => write!(f, "flows are already linked"),
107 Self::EmptyName => write!(f, "registry name must be non-empty"),
108 Self::SelfRelation => write!(f, "cannot link or monitor a flow to itself"),
109 Self::Unavailable => write!(f, "runtime table unavailable (poisoned lock)"),
110 }
111 }
112}
113
114impl std::error::Error for LifecycleError {}
115
116/// Record an infrastructure fault (stderr + counter). Does not panic.
117#[cold]
118pub fn report_fault(err: RuntimeError) {
119 FAULTS.fetch_add(1, Ordering::Relaxed);
120 eprintln!("byteflow: {err} — fail-closed");
121}
122
123/// How many [`report_fault`] calls have been made (tests / diagnostics).
124pub fn fault_count() -> u64 {
125 FAULTS.load(Ordering::Relaxed)
126}
127
128/// User-facing spawn / load errors (category A in the taxonomy above).
129///
130/// These used to be `.expect(...)` panics on `Runtime::new` / `spawn` /
131/// OS thread creation. Panic is reserved for *bugs*; a bad function index
132/// or a refused `thread::spawn` is an embedder-visible failure and must
133/// surface as `Result` so the host can recover without taking workers down.
134#[derive(Debug, Clone, PartialEq, Eq)]
135pub enum SpawnError {
136 /// `function` index is outside the runtime chunk's function table.
137 BadFunction { index: u32, table_size: u32 },
138 /// Chunk failed verification before the runtime could start.
139 VerifyFailed(crate::bytecode::VerifyError),
140 /// Spawn arguments contained an unknown or unusable capability.
141 InvalidCapability,
142 /// Scheduler table poisoned (fail closed).
143 Unavailable,
144 /// Live flow count would exceed [`crate::RuntimeConfig::max_flows`].
145 FlowLimit { current: usize, max: u32 },
146 /// Bytecode spawn failed the quota / SPAWN-right / attenuation check.
147 SpawnDenied(String),
148 /// [`crate::ChildSpec::name`] is already in the runtime registry.
149 NameTaken { name: String },
150 /// OS refused to create a worker / timer / supervisor thread.
151 ThreadSpawnFailed(String),
152 /// `Vm::new` failed for a reason other than a bad function index
153 /// (mapped from [`crate::Fault`] via `From`).
154 VmInit(String),
155}
156
157impl fmt::Display for SpawnError {
158 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159 match self {
160 SpawnError::BadFunction { index, table_size } => {
161 write!(
162 f,
163 "spawn: function index {index} out of range (table size {table_size})"
164 )
165 }
166 SpawnError::VerifyFailed(err) => write!(f, "chunk verification failed: {err}"),
167 SpawnError::InvalidCapability => {
168 write!(f, "spawn: argument capability is unknown or not held")
169 }
170 SpawnError::Unavailable => write!(f, "spawn: runtime table unavailable"),
171 SpawnError::FlowLimit { current, max } => {
172 write!(f, "spawn: live flow limit reached ({current}/{max})")
173 }
174 SpawnError::SpawnDenied(msg) => write!(f, "spawn: {msg}"),
175 SpawnError::NameTaken { name } => {
176 write!(f, "spawn: registry name {name:?} already taken")
177 }
178 SpawnError::ThreadSpawnFailed(msg) => {
179 write!(f, "failed to spawn runtime thread: {msg}")
180 }
181 SpawnError::VmInit(msg) => write!(f, "vm init failed: {msg}"),
182 }
183 }
184}
185
186impl std::error::Error for SpawnError {}
187
188impl From<crate::vm::Fault> for SpawnError {
189 fn from(fault: crate::vm::Fault) -> Self {
190 match fault {
191 crate::vm::Fault::BadFunction { index, table_size } => {
192 SpawnError::BadFunction { index, table_size }
193 }
194 other => SpawnError::VmInit(other.to_string()),
195 }
196 }
197}