Skip to main content

scientific_workflow/runtime/
error.rs

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