Skip to main content

moirai_core/
error.rs

1//! Error types and handling for the Moirai runtime.
2
3use core::fmt;
4
5/// Errors that can occur during task operations.
6#[allow(clippy::module_name_repetitions)]
7#[must_use]
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum TaskError {
10    /// Task was cancelled before completion
11    Cancelled,
12    /// Task panicked during execution
13    Panicked,
14    /// Task exceeded its execution time limit
15    Timeout,
16    /// Task failed due to resource exhaustion
17    ResourceExhausted,
18    /// Task failed due to an invalid operation
19    InvalidOperation,
20    /// Generic task execution error
21    ExecutionFailed(TaskErrorKind),
22    /// Task execution timed out waiting for completion
23    ExecutionTimeout,
24    /// Task result was not found in storage
25    ResultNotFound,
26    /// Task failed to spawn
27    SpawnFailed,
28    /// Task is not in a valid state for the operation
29    InvalidState,
30    /// Task has already completed
31    AlreadyCompleted,
32}
33
34/// Specific kinds of task execution errors.
35#[must_use]
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum TaskErrorKind {
38    /// I/O operation failed
39    Io,
40    /// Network operation failed
41    Network,
42    /// File system operation failed
43    FileSystem,
44    /// Permission denied
45    PermissionDenied,
46    /// Resource not found
47    NotFound,
48    /// Operation would block
49    WouldBlock,
50    /// Operation interrupted
51    Interrupted,
52    /// Invalid input provided
53    InvalidInput,
54    /// Other error
55    Other,
56}
57
58impl fmt::Display for TaskError {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        match self {
61            Self::Cancelled => write!(f, "Task was cancelled"),
62            Self::Panicked => write!(f, "Task panicked during execution"),
63            Self::ExecutionTimeout => write!(f, "Task execution timed out"),
64            Self::ResultNotFound => write!(f, "Task result not found"),
65            Self::SpawnFailed => write!(f, "Task failed to spawn"),
66            Self::Timeout => write!(f, "Task exceeded execution time limit"),
67            Self::ResourceExhausted => write!(f, "Task failed due to resource exhaustion"),
68            Self::InvalidOperation => write!(f, "Invalid operation"),
69            Self::ExecutionFailed(kind) => write!(f, "Task execution failed: {kind}"),
70            Self::InvalidState => write!(f, "Task is not in a valid state for the operation"),
71            Self::AlreadyCompleted => write!(f, "Task has already completed"),
72        }
73    }
74}
75
76impl fmt::Display for TaskErrorKind {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        match self {
79            Self::Io => write!(f, "I/O error"),
80            Self::Network => write!(f, "Network error"),
81            Self::FileSystem => write!(f, "File system error"),
82            Self::PermissionDenied => write!(f, "Permission denied"),
83            Self::NotFound => write!(f, "Resource not found"),
84            Self::WouldBlock => write!(f, "Operation would block"),
85            Self::Interrupted => write!(f, "Operation interrupted"),
86            Self::InvalidInput => write!(f, "Invalid input"),
87            Self::Other => write!(f, "Other error"),
88        }
89    }
90}
91
92/// Errors that can occur during executor operations.
93#[allow(clippy::module_name_repetitions)]
94#[must_use]
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub enum ExecutorError {
97    /// Executor is shutting down
98    ShuttingDown,
99    /// Executor is already running
100    AlreadyRunning,
101    /// Executor configuration is invalid
102    InvalidConfiguration,
103    /// The requested local queue initial capacity cannot form a supported allocation.
104    InvalidLocalQueueInitialCapacity {
105        /// Requested slot count before power-of-two normalization.
106        requested: usize,
107    },
108    /// Thread pool creation failed
109    ThreadPoolCreationFailed,
110    /// Task spawn failed
111    SpawnFailed(TaskError),
112    /// Resource exhaustion detected
113    ResourceExhausted(String),
114    /// Performance anomaly detected
115    PerformanceAnomaly(String),
116    /// No scheduler available
117    NoSchedulerAvailable,
118    /// Scheduler error
119    SchedulerError(SchedulerError),
120    /// A worker could not be confined to its planned logical processor.
121    ///
122    /// Reported by construction under
123    /// [`WorkerPlacement::Pinned`](crate::executor::WorkerPlacement::Pinned).
124    /// When several workers fail, the lowest-numbered one is named. The
125    /// scheduler was not started: no worker outlives the failed construction.
126    WorkerPlacementFailed {
127        /// Index of the worker whose binding failed.
128        worker: usize,
129        /// Flattened logical processor id the worker was planned onto.
130        processor: u32,
131        /// Why the binding did not take effect.
132        cause: PlacementFailure,
133    },
134}
135
136/// Why a worker could not be confined to a logical processor.
137///
138/// Every variant leaves the thread with the affinity it had before the attempt.
139#[must_use]
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141#[non_exhaustive]
142pub enum PlacementFailure {
143    /// This target has no thread-binding backend.
144    Unsupported,
145    /// The processor id is beyond what the target affinity interface can name.
146    OutOfRange,
147    /// The operating system refused the request, for example because the
148    /// processor is offline or outside the allowed set of the process.
149    Os {
150        /// Raw operating-system error code: `GetLastError` on Windows,
151        /// `errno` on Linux.
152        code: i32,
153    },
154}
155
156impl fmt::Display for ExecutorError {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        match self {
159            Self::ShuttingDown => write!(f, "Executor is shutting down"),
160            Self::AlreadyRunning => write!(f, "Executor is already running"),
161            Self::InvalidConfiguration => write!(f, "Invalid executor configuration"),
162            Self::InvalidLocalQueueInitialCapacity { requested } => write!(
163                f,
164                "local queue initial capacity {requested} cannot form a supported allocation"
165            ),
166            Self::ThreadPoolCreationFailed => write!(f, "Failed to create thread pool"),
167            Self::SpawnFailed(err) => write!(f, "Failed to spawn task: {err}"),
168            Self::ResourceExhausted(msg) => write!(f, "Resource exhausted: {msg}"),
169            Self::PerformanceAnomaly(msg) => write!(f, "Performance anomaly: {msg}"),
170            Self::NoSchedulerAvailable => write!(f, "No scheduler available"),
171            Self::SchedulerError(err) => write!(f, "Scheduler error: {err}"),
172            Self::WorkerPlacementFailed {
173                worker, processor, ..
174            } => write!(
175                f,
176                "worker {worker} could not be pinned to logical processor {processor}"
177            ),
178        }
179    }
180}
181
182impl fmt::Display for PlacementFailure {
183    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184        match self {
185            Self::Unsupported => write!(f, "thread binding is unsupported on this target"),
186            Self::OutOfRange => {
187                write!(f, "the processor is outside the range this target can bind")
188            }
189            Self::Os { code } => write!(
190                f,
191                "the operating system refused the binding (error code {code})"
192            ),
193        }
194    }
195}
196
197/// Errors that can occur during scheduler operations.
198#[allow(clippy::module_name_repetitions)]
199#[must_use]
200#[derive(Debug, Clone, PartialEq, Eq)]
201pub enum SchedulerError {
202    /// Queue is full and cannot accept more tasks
203    QueueFull,
204    /// Queue is empty
205    QueueEmpty,
206    /// Work stealing failed
207    StealFailed,
208    /// Invalid scheduler state
209    InvalidState,
210    /// System failure occurred
211    SystemFailure(String),
212    /// Invalid scheduler reference
213    InvalidScheduler,
214}
215
216impl fmt::Display for SchedulerError {
217    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218        match self {
219            Self::QueueFull => write!(f, "Task queue is full"),
220            Self::QueueEmpty => write!(f, "Task queue is empty"),
221            Self::StealFailed => write!(f, "Work stealing failed"),
222            Self::InvalidState => write!(f, "Invalid scheduler state"),
223            Self::SystemFailure(msg) => write!(f, "System failure: {msg}"),
224            Self::InvalidScheduler => write!(f, "Invalid scheduler reference"),
225        }
226    }
227}
228
229/// A result type for task operations.
230pub type TaskResult<T> = Result<T, TaskError>;
231
232/// A result type for executor operations.
233pub type ExecutorResult<T> = Result<T, ExecutorError>;
234
235/// A result type for scheduler operations.
236pub type SchedulerResult<T> = Result<T, SchedulerError>;
237
238#[cfg(feature = "std")]
239impl std::error::Error for TaskError {}
240
241#[cfg(feature = "std")]
242impl std::error::Error for ExecutorError {
243    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
244        match self {
245            Self::WorkerPlacementFailed { cause, .. } => Some(cause),
246            _ => None,
247        }
248    }
249}
250
251#[cfg(feature = "std")]
252impl std::error::Error for PlacementFailure {}
253
254#[cfg(feature = "std")]
255impl std::error::Error for SchedulerError {}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn test_error_display() {
263        assert_eq!(format!("{}", TaskError::Cancelled), "Task was cancelled");
264        assert_eq!(
265            format!("{}", TaskError::ExecutionFailed(TaskErrorKind::Io)),
266            "Task execution failed: I/O error"
267        );
268        assert_eq!(
269            format!("{}", ExecutorError::ShuttingDown),
270            "Executor is shutting down"
271        );
272        assert_eq!(
273            format!("{}", SchedulerError::QueueFull),
274            "Task queue is full"
275        );
276    }
277
278    #[cfg(feature = "std")]
279    #[test]
280    fn placement_failure_names_worker_processor_and_keeps_cause_as_source() {
281        let error = ExecutorError::WorkerPlacementFailed {
282            worker: 3,
283            processor: 17,
284            cause: PlacementFailure::Os { code: 87 },
285        };
286        assert_eq!(
287            format!("{error}"),
288            "worker 3 could not be pinned to logical processor 17"
289        );
290        let source = std::error::Error::source(&error).expect("invariant: cause is the source");
291        assert_eq!(
292            format!("{source}"),
293            "the operating system refused the binding (error code 87)"
294        );
295        assert!(std::error::Error::source(&ExecutorError::ShuttingDown).is_none());
296        assert_eq!(
297            format!("{}", PlacementFailure::Unsupported),
298            "thread binding is unsupported on this target"
299        );
300        assert_eq!(
301            format!("{}", PlacementFailure::OutOfRange),
302            "the processor is outside the range this target can bind"
303        );
304    }
305}