Skip to main content

scientific_workflow/runtime/
error.rs

1//! Errors produced by runtime planning, scheduling, and rendering.
2
3use std::io;
4
5use thiserror::Error;
6
7use crate::configuration::ConfigurationError;
8
9/// Failure while planning, executing, or displaying a workflow runtime.
10#[derive(Debug, Error)]
11#[non_exhaustive]
12pub enum RuntimeError {
13    /// A selected phase failed after producing a structured runtime snapshot.
14    #[error("workflow phase execution failed: {source}")]
15    PhaseExecutionFailed {
16        /// All phase outcomes observed through the failed phase.
17        summary: super::RuntimeSummary,
18        /// Exact scheduling, cancellation, panic, or workload cause.
19        #[source]
20        source: Box<RuntimeError>,
21    },
22
23    /// Project configuration could not supply a required task or identity value.
24    #[error(transparent)]
25    Configuration(#[from] ConfigurationError),
26
27    /// Another live reporter already owns process terminal rendering.
28    #[error("another progress reporter already owns the process terminal")]
29    TerminalAlreadyOwned,
30
31    /// The validated task count cannot be represented by this platform.
32    #[error("task count {task_count} exceeds this platform's addressable progress slots")]
33    TaskCountTooLarge {
34        /// Validated project task count.
35        task_count: u64,
36    },
37
38    /// One phase contains no tasks.
39    #[error("phase {phase} must contain at least one task")]
40    EmptyPhase { phase: u64 },
41
42    /// A first-class reporter/runtime plan contains no phase.
43    #[error("at least one phase is required")]
44    EmptyPhaseSet,
45
46    /// A phase label is empty or whitespace-only.
47    #[error("phase {phase} must have a nonempty label")]
48    InvalidPhaseLabel { phase: u64 },
49
50    /// A phase active-workload limit is zero.
51    #[error("phase {phase} max_concurrent_workloads must be greater than zero")]
52    InvalidPhaseWorkloadLimit { phase: u64 },
53
54    /// A phase prepared-work queue capacity is zero.
55    #[error("phase {phase} queue_capacity must be greater than zero")]
56    InvalidPhaseQueueCapacity { phase: u64 },
57
58    /// A reporter phase list repeats one phase ID.
59    #[error("phase ID {phase} appears more than once")]
60    DuplicatePhaseId { phase: u64 },
61
62    /// One phase dependency is not registered in the runtime plan.
63    #[error("phase {phase} depends on unknown phase {dependency}")]
64    UnknownPhaseDependency { phase: u64, dependency: u64 },
65
66    /// The phase dependency graph contains a cycle.
67    #[error("phase dependency graph contains a cycle involving phase {phase}")]
68    PhaseDependencyCycle { phase: u64 },
69
70    /// A selected phase ID is absent from the runtime plan.
71    #[error("selected phase {phase} is not registered")]
72    UnknownSelectedPhase { phase: u64 },
73
74    /// Exact selection omitted a dependency that was not externally verified.
75    #[error("selected phase {phase} requires unsatisfied phase {dependency}")]
76    UnsatisfiedPhaseDependency { phase: u64, dependency: u64 },
77
78    /// Standard input ended before a required phase transition was confirmed.
79    #[error("confirmation input ended after phase {phase} before the next phase could start")]
80    PhaseConfirmationEof { phase: u64 },
81
82    /// A required phase-transition confirmation could not read standard input.
83    #[error("failed to read confirmation after phase {phase}")]
84    PhaseConfirmationInput {
85        /// Successfully completed phase awaiting permission to advance.
86        phase: u64,
87        /// Underlying standard-input or prompt-output failure.
88        #[source]
89        source: io::Error,
90    },
91
92    /// One declared task has no executable workload.
93    #[error("task `{task}` has no workload")]
94    MissingTaskWorkload { task: String },
95
96    /// A task-owned workload returned an error.
97    #[error("task `{task}` failed: {source}")]
98    TaskWorkload {
99        task: String,
100        #[source]
101        source: Box<dyn std::error::Error + Send + Sync + 'static>,
102    },
103
104    /// A scheduler worker panicked.
105    #[error("a runtime scheduler worker panicked")]
106    SchedulerPanicked,
107
108    /// Cooperative cancellation stopped the selected runtime plan.
109    #[error("workflow runtime was cancelled")]
110    Cancelled,
111
112    /// One task has an empty phase-local ID.
113    #[error("phase {phase} contains an empty task ID")]
114    InvalidManagedTaskId { phase: u64 },
115
116    /// One task has an empty kind/namespace.
117    #[error("task `{task}` must have a nonempty kind")]
118    InvalidManagedTaskKind { task: String },
119
120    /// One phase repeats the same phase-local task ID.
121    #[error("phase {phase} repeats task ID `{task}`")]
122    DuplicateManagedTaskId { phase: u64, task: String },
123
124    /// Tasks of one kind do not expose one consistent parameter-key set.
125    #[error("task kind `{kind}` has inconsistent parameter keys between `{first}` and `{second}`")]
126    InconsistentManagedTaskParameters {
127        kind: String,
128        first: String,
129        second: String,
130    },
131
132    /// A display projection names a task kind absent from the phase.
133    #[error("task kind `{kind}` is not declared by the phase")]
134    UnknownManagedTaskKind { kind: String },
135
136    /// Two tasks receive the same requested generated label.
137    #[error("generated task label `{label}` collides between `{first}` and `{second}`")]
138    ManagedTaskDisplayCollision {
139        label: String,
140        first: String,
141        second: String,
142    },
143
144    /// A partial selector matched no managed task.
145    #[error("task selector `{selector}` matched no task")]
146    ManagedTaskNotFound { selector: String },
147
148    /// A partial selector matched more than one managed task.
149    #[error("task selector `{selector}` is ambiguous between `{first}` and `{second}`")]
150    ManagedTaskSelectorAmbiguous {
151        selector: String,
152        first: String,
153        second: String,
154    },
155
156    /// A managed task does not contain one required parameter.
157    #[error("task `{task}` does not contain parameter `{key}`")]
158    UnknownManagedTaskParameter { task: String, key: String },
159
160    /// One managed task parameter could not be decoded.
161    #[error("task `{task}` parameter `{key}` could not be decoded")]
162    DecodeManagedTaskParameter {
163        task: String,
164        key: String,
165        #[source]
166        source: serde_json::Error,
167    },
168
169    /// An explicit parameter key is empty.
170    #[error("task parameter key `{key}` is invalid")]
171    InvalidTaskParameter { key: String },
172
173    /// Configuration-derived parameters cannot be mutated.
174    #[error("configuration-derived task `{task}` has immutable parameters")]
175    ConfiguredTaskParametersImmutable { task: String },
176
177    /// The reporter does not contain one exact first-class task key.
178    #[error("managed task `{task}` does not exist in this reporter")]
179    UnknownManagedTask { task: String },
180
181    /// A requested handle does not match the task's declared display kind.
182    #[error("task `{task}` is declared as {actual}, not {requested}")]
183    ManagedTaskKindMismatch {
184        task: String,
185        requested: &'static str,
186        actual: &'static str,
187    },
188
189    /// One identity key was supplied more than once.
190    #[error("task identity repeats parameter key `{key}`")]
191    DuplicateIdentityParameter {
192        /// Repeated exact parameter name.
193        key: String,
194    },
195
196    /// One requested identity key is absent from project parameters.
197    #[error("task identity parameter `{key}` is not declared by the project")]
198    UnknownIdentityParameter {
199        /// Missing exact parameter name.
200        key: String,
201    },
202
203    /// Two generated tasks have the same selected parameter identity.
204    #[error(
205        "task identity `{identity}` is shared by ordinals {first_ordinal} and {second_ordinal}"
206    )]
207    NonUniqueTaskIdentity {
208        /// Deterministic rendered parameter identity.
209        identity: String,
210        /// First colliding automatically assigned ordinal.
211        first_ordinal: u64,
212        /// Second colliding automatically assigned ordinal.
213        second_ordinal: u64,
214    },
215
216    /// A task handle does not belong to the reporter's configured task space.
217    #[error("task ordinal {task_ordinal} is outside the reporter's task registry")]
218    UnknownTaskOrdinal {
219        /// Automatically assigned ordinal obtained from `TaskConfig`.
220        task_ordinal: u64,
221    },
222
223    /// A directly registered application task name was not found.
224    #[error("registered task `{identity}` does not exist")]
225    UnknownRegisteredTask { identity: String },
226
227    /// The same directly registered application task name appeared twice.
228    #[error("registered task `{identity}` appears more than once")]
229    DuplicateRegisteredTask { identity: String },
230
231    /// A task handle's selected identity differs from the registered project.
232    #[error("task ordinal {task_ordinal} does not match its registered parameter identity")]
233    TaskIdentityMismatch {
234        /// Automatically assigned task ordinal.
235        task_ordinal: u64,
236    },
237
238    /// The same task was started more than once.
239    #[error("task `{identity}` has already started or reached a terminal status")]
240    TaskAlreadyStarted {
241        /// Human-readable parameter identity.
242        identity: String,
243    },
244
245    /// Initial progress lies beyond a known target.
246    #[error("task `{identity}` starts at iteration {initial}, beyond target {target}")]
247    InitialIterationBeyondTarget {
248        /// Human-readable parameter identity.
249        identity: String,
250        /// Initial absolute simulation iteration.
251        initial: u64,
252        /// Target absolute simulation iteration.
253        target: u64,
254    },
255
256    /// A progress update attempted to move scientific iteration backward.
257    #[error("task `{identity}` cannot move progress from iteration {current} back to {attempted}")]
258    IterationRegressed {
259        /// Human-readable parameter identity.
260        identity: String,
261        /// Previously reported iteration.
262        current: u64,
263        /// Rejected iteration.
264        attempted: u64,
265    },
266
267    /// A progress update exceeded a known target.
268    #[error("task `{identity}` reported iteration {iteration}, beyond target {target}")]
269    IterationBeyondTarget {
270        /// Human-readable parameter identity.
271        identity: String,
272        /// Rejected absolute simulation iteration.
273        iteration: u64,
274        /// Configured absolute target iteration.
275        target: u64,
276    },
277
278    /// Completion was requested before a known target was reached.
279    #[error("task `{identity}` completed at iteration {current}, before target {target}")]
280    TargetIterationNotReached {
281        /// Human-readable parameter identity.
282        identity: String,
283        /// Last reported absolute simulation iteration.
284        current: u64,
285        /// Configured absolute target iteration.
286        target: u64,
287    },
288
289    /// The sole renderer thread could not be created.
290    #[error("failed to start the centralized terminal reporter")]
291    StartRenderer {
292        /// Underlying thread-creation failure.
293        #[source]
294        source: io::Error,
295    },
296
297    /// Interactive terminal isolation could not be established.
298    #[error("failed to {operation} for the isolated progress screen")]
299    TerminalSetup {
300        operation: &'static str,
301        #[source]
302        source: io::Error,
303    },
304
305    /// The renderer stopped before accepting a requested message.
306    #[error("the centralized terminal reporter is no longer available")]
307    RendererUnavailable,
308
309    /// The renderer thread panicked while the reporter was active.
310    #[error("the centralized terminal reporter panicked")]
311    RendererPanicked,
312
313    /// Successful finalization was requested before every task completed.
314    #[error(
315        "cannot report success with {pending} pending, {running} running, and {failed} failed tasks"
316    )]
317    IncompleteProgress {
318        /// Tasks that never started.
319        pending: u64,
320        /// Tasks that have not reached a terminal status.
321        running: u64,
322        /// Tasks that failed or dropped before completion.
323        failed: u64,
324    },
325}
326
327impl RuntimeError {
328    /// Returns structured outcomes when execution reached a failing phase.
329    pub fn runtime_summary(&self) -> Option<&super::RuntimeSummary> {
330        match self {
331            Self::PhaseExecutionFailed { summary, .. } => Some(summary),
332            _ => None,
333        }
334    }
335
336    /// Returns the underlying execution cause when a phase failed.
337    pub fn execution_cause(&self) -> Option<&RuntimeError> {
338        match self {
339            Self::PhaseExecutionFailed { source, .. } => Some(source),
340            _ => None,
341        }
342    }
343}
344
345pub(crate) use RuntimeError as ReportingError;