Skip to main content

scientific_workflow/study/
error.rs

1//! Errors produced while planning, scheduling, and rendering a study.
2
3use std::io;
4use std::path::PathBuf;
5use std::time::Duration;
6
7use thiserror::Error;
8
9/// Failure while validating, executing, recording, or displaying a study.
10#[derive(Debug, Error)]
11#[non_exhaustive]
12pub enum StudyError {
13    /// A completed study record could not be encoded as JSON.
14    #[error("failed to serialize study record")]
15    SerializeStudyRecord {
16        /// Underlying Serde serialization failure.
17        #[source]
18        source: serde_json::Error,
19    },
20    /// A serialized study record could not be written durably.
21    #[error("failed to write study record `{path}`")]
22    WriteStudyRecord {
23        /// Destination study-record path.
24        path: PathBuf,
25        /// Underlying filesystem failure.
26        #[source]
27        source: io::Error,
28    },
29    /// A UTC timestamp required by a study record could not be formatted.
30    #[error("failed to format UTC timestamp while attempting to {operation}")]
31    StudyRecordTimestamp {
32        /// Record operation that requested the timestamp.
33        operation: &'static str,
34        /// Underlying time-formatting failure.
35        #[source]
36        source: time::error::Format,
37    },
38    /// A study plan could not be encoded as JSON.
39    #[error("failed to serialize study plan")]
40    SerializeStudyPlan {
41        /// Underlying Serde serialization failure.
42        #[source]
43        source: serde_json::Error,
44    },
45    /// A serialized study plan could not be written durably.
46    #[error("failed to write study plan `{path}`")]
47    WriteStudyPlan {
48        /// Destination study-plan path.
49        path: PathBuf,
50        /// Underlying filesystem failure.
51        #[source]
52        source: io::Error,
53    },
54    /// A plan destination already contains a different immutable declaration.
55    #[error("study plan destination `{path}` already contains different data")]
56    StudyPlanConflict {
57        /// Path containing the conflicting plan.
58        path: PathBuf,
59    },
60    /// A phase failed after producing a durable partial study summary.
61    #[error("study phase execution failed: {source}")]
62    PhaseExecutionFailed {
63        /// Summary and record containing all completed phase outcomes.
64        summary: super::StudySummary,
65        /// Original phase execution failure.
66        #[source]
67        source: Box<StudyError>,
68    },
69    /// Another study currently owns the process-wide terminal renderer.
70    #[error("another study renderer already owns the process terminal")]
71    TerminalAlreadyOwned,
72    /// A phase was declared without tasks.
73    #[error("phase {phase} must contain at least one task")]
74    EmptyPhase {
75        /// Invalid phase identity.
76        phase: u64,
77    },
78    /// A study was declared without phases.
79    #[error("a study must contain at least one phase")]
80    EmptyPhaseSet,
81    /// A phase label is empty or whitespace-only.
82    #[error("phase {phase} must have a nonempty label")]
83    InvalidPhaseLabel {
84        /// Invalid phase identity.
85        phase: u64,
86    },
87    /// A phase declares a zero active-task limit.
88    #[error("phase {phase} max_active_tasks must be greater than zero")]
89    InvalidPhaseWorkloadLimit {
90        /// Invalid phase identity.
91        phase: u64,
92    },
93    /// A phase declares a zero prepared-task queue capacity.
94    #[error("phase {phase} prepared_task_queue_capacity must be greater than zero")]
95    InvalidPhaseQueueCapacity {
96        /// Invalid phase identity.
97        phase: u64,
98    },
99    /// A phase timing setting is zero or cannot be represented internally.
100    #[error("phase {phase} timing setting `{setting}` must be nonzero and representable")]
101    InvalidPhaseTiming {
102        /// Invalid phase identity.
103        phase: u64,
104        /// Name of the rejected timing setting.
105        setting: &'static str,
106    },
107    /// Two phases have the same stable identity.
108    #[error("phase ID {phase} appears more than once")]
109    DuplicatePhaseId {
110        /// Repeated phase identity.
111        phase: u64,
112    },
113    /// A phase depends on an undeclared phase.
114    #[error("phase {phase} depends on unknown phase {dependency}")]
115    UnknownPhaseDependency {
116        /// Depending phase identity.
117        phase: u64,
118        /// Missing dependency identity.
119        dependency: u64,
120    },
121    /// The declared phase dependency graph is cyclic.
122    #[error("phase dependency graph contains a cycle involving phase {phase}")]
123    PhaseDependencyCycle {
124        /// One phase involved in the cycle.
125        phase: u64,
126    },
127    /// Execution selected a phase that is not declared.
128    #[error("selected phase {phase} is not registered")]
129    UnknownSelectedPhase {
130        /// Missing selected phase identity.
131        phase: u64,
132    },
133    /// Execution omitted a required dependency of a selected phase.
134    #[error("selected phase {phase} requires unsatisfied phase {dependency}")]
135    UnsatisfiedPhaseDependency {
136        /// Selected phase identity.
137        phase: u64,
138        /// Omitted dependency identity.
139        dependency: u64,
140    },
141    /// Interactive confirmation input ended before another phase could start.
142    #[error("confirmation input ended after phase {phase} before the next phase could start")]
143    PhaseConfirmationEof {
144        /// Last completed phase identity.
145        phase: u64,
146    },
147    /// Interactive confirmation input could not be read.
148    #[error("failed to read confirmation after phase {phase}")]
149    PhaseConfirmationInput {
150        /// Last completed phase identity.
151        phase: u64,
152        /// Underlying input failure.
153        #[source]
154        source: io::Error,
155    },
156    /// An executable task declaration has no workload.
157    #[error("task `{task}` has no workload")]
158    MissingTaskWorkload {
159        /// Phase-qualified task identity.
160        task: String,
161    },
162    /// An application-owned task workload returned an error.
163    #[error("task `{task}` failed: {source}")]
164    TaskWorkload {
165        /// Phase-qualified task identity.
166        task: String,
167        /// Application-owned workload failure.
168        #[source]
169        source: Box<dyn std::error::Error + Send + Sync + 'static>,
170    },
171    /// A task did not stop cooperatively before its timeout.
172    #[error("task `{task}` exceeded its timeout of {timeout:?}")]
173    TaskTimedOut {
174        /// Phase-qualified task identity.
175        task: String,
176        /// Configured task timeout.
177        timeout: Duration,
178    },
179    /// A phase did not stop cooperatively before its deadline.
180    #[error("phase {phase} exceeded its deadline of {deadline_after:?}")]
181    PhaseDeadlineExceeded {
182        /// Expired phase identity.
183        phase: u64,
184        /// Configured phase deadline relative to its start.
185        deadline_after: Duration,
186    },
187    /// A scheduler worker thread panicked.
188    #[error("a study scheduler worker panicked")]
189    SchedulerPanicked,
190    /// Study execution was cancelled cooperatively.
191    #[error("study execution was cancelled")]
192    Cancelled,
193    /// A task ID is empty or whitespace-only.
194    #[error("phase {phase} contains an empty task ID")]
195    InvalidTaskId {
196        /// Phase containing the invalid task.
197        phase: u64,
198    },
199    /// A task category is empty or whitespace-only.
200    #[error("task `{task}` must have a nonempty category")]
201    InvalidTaskCategory {
202        /// Phase-qualified invalid task identity.
203        task: String,
204    },
205    /// A phase repeats a task ID.
206    #[error("phase {phase} repeats task ID `{task}`")]
207    DuplicateTaskId {
208        /// Phase containing the duplicate.
209        phase: u64,
210        /// Repeated phase-local task ID.
211        task: String,
212    },
213    /// A task selector matched no declared task.
214    #[error("task selector `{selector}` matched no task")]
215    TaskNotFound {
216        /// Unmatched selector text.
217        selector: String,
218    },
219    /// An unqualified selector matched more than one task.
220    #[error("task selector `{selector}` is ambiguous between `{first}` and `{second}`")]
221    TaskSelectorAmbiguous {
222        /// Ambiguous selector text.
223        selector: String,
224        /// First matching phase-qualified task identity.
225        first: String,
226        /// Second matching phase-qualified task identity.
227        second: String,
228    },
229    /// A task does not contain requested metadata.
230    #[error("task `{task}` does not contain metadata `{key}`")]
231    UnknownTaskMetadata {
232        /// Phase-qualified task identity.
233        task: String,
234        /// Missing metadata key.
235        key: String,
236    },
237    /// Task metadata could not be decoded as the requested type.
238    #[error("task `{task}` metadata `{key}` could not be decoded")]
239    DecodeTaskMetadata {
240        /// Phase-qualified task identity.
241        task: String,
242        /// Metadata key being decoded.
243        key: String,
244        /// Underlying Serde conversion failure.
245        #[source]
246        source: serde_json::Error,
247    },
248    /// A progress operation referenced an unregistered task.
249    #[error("task `{task}` is not registered with this renderer")]
250    UnknownTask {
251        /// Unregistered phase-qualified task identity.
252        task: String,
253    },
254    /// A progress operation is incompatible with the task's declared mode.
255    #[error("task `{task}` is declared as {actual}, not {requested}")]
256    TaskModeMismatch {
257        /// Phase-qualified task identity.
258        task: String,
259        /// Mode required by the attempted operation.
260        requested: &'static str,
261        /// Mode declared by the task.
262        actual: &'static str,
263    },
264    /// A task was started after already leaving its pending state.
265    #[error("task `{identity}` has already started or reached a terminal status")]
266    TaskAlreadyStarted {
267        /// Phase-qualified task identity.
268        identity: String,
269    },
270    /// Initial progress is greater than the declared target iteration.
271    #[error("task `{identity}` starts at iteration {initial}, beyond target {target}")]
272    InitialIterationBeyondTarget {
273        /// Phase-qualified task identity.
274        identity: String,
275        /// Rejected initial iteration.
276        initial: u64,
277        /// Declared target iteration.
278        target: u64,
279    },
280    /// A task attempted to report a lower iteration than previously observed.
281    #[error("task `{identity}` cannot move progress from iteration {current} back to {attempted}")]
282    IterationRegressed {
283        /// Phase-qualified task identity.
284        identity: String,
285        /// Last accepted iteration.
286        current: u64,
287        /// Rejected lower iteration.
288        attempted: u64,
289    },
290    /// A task reported progress beyond its target iteration.
291    #[error("task `{identity}` reported iteration {iteration}, beyond target {target}")]
292    IterationBeyondTarget {
293        /// Phase-qualified task identity.
294        identity: String,
295        /// Rejected reported iteration.
296        iteration: u64,
297        /// Declared target iteration.
298        target: u64,
299    },
300    /// A task reported completion before reaching its target iteration.
301    #[error("task `{identity}` completed at iteration {current}, before target {target}")]
302    TargetIterationNotReached {
303        /// Phase-qualified task identity.
304        identity: String,
305        /// Last accepted iteration.
306        current: u64,
307        /// Declared target iteration.
308        target: u64,
309    },
310    /// The renderer thread could not be created.
311    #[error("failed to start the study renderer")]
312    StartRenderer {
313        /// Underlying thread-creation failure.
314        #[source]
315        source: io::Error,
316    },
317    /// The process terminal could not be prepared or restored.
318    #[error("failed to {operation} for the study display")]
319    TerminalSetup {
320        /// Stable terminal operation description.
321        operation: &'static str,
322        /// Underlying terminal IO failure.
323        #[source]
324        source: io::Error,
325    },
326    /// The renderer stopped accepting progress commands unexpectedly.
327    #[error("the study renderer is no longer available")]
328    RendererUnavailable,
329    /// The renderer thread panicked.
330    #[error("the study renderer panicked")]
331    RendererPanicked,
332    /// A success summary was requested while non-success task states remain.
333    #[error(
334        "cannot report success with {pending} pending, {running} running, and {failed} failed tasks"
335    )]
336    IncompleteProgress {
337        /// Number of tasks that never started.
338        pending: u64,
339        /// Number of tasks still running.
340        running: u64,
341        /// Number of failed tasks.
342        failed: u64,
343    },
344}
345
346impl StudyError {
347    /// Returns the durable partial summary attached to a phase execution failure.
348    pub fn study_summary(&self) -> Option<&super::StudySummary> {
349        match self {
350            Self::PhaseExecutionFailed { summary, .. } => Some(summary),
351            _ => None,
352        }
353    }
354
355    /// Returns the original cause attached to a phase execution failure.
356    pub fn execution_cause(&self) -> Option<&StudyError> {
357        match self {
358            Self::PhaseExecutionFailed { source, .. } => Some(source),
359            _ => None,
360        }
361    }
362}