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#[derive(Debug, Error)]
10#[non_exhaustive]
11pub enum StudyError {
12    #[error("failed to serialize study record")]
13    SerializeStudyRecord {
14        #[source]
15        source: serde_json::Error,
16    },
17    #[error("failed to write study record `{path}`")]
18    WriteStudyRecord {
19        path: PathBuf,
20        #[source]
21        source: io::Error,
22    },
23    #[error("failed to format UTC timestamp while attempting to {operation}")]
24    StudyRecordTimestamp {
25        operation: &'static str,
26        #[source]
27        source: time::error::Format,
28    },
29    #[error("failed to serialize study plan")]
30    SerializeStudyPlan {
31        #[source]
32        source: serde_json::Error,
33    },
34    #[error("failed to write study plan `{path}`")]
35    WriteStudyPlan {
36        path: PathBuf,
37        #[source]
38        source: io::Error,
39    },
40    #[error("study plan destination `{path}` already contains different data")]
41    StudyPlanConflict { path: PathBuf },
42    #[error("study phase execution failed: {source}")]
43    PhaseExecutionFailed {
44        summary: super::StudySummary,
45        #[source]
46        source: Box<StudyError>,
47    },
48    #[error("another study renderer already owns the process terminal")]
49    TerminalAlreadyOwned,
50    #[error("phase {phase} must contain at least one task")]
51    EmptyPhase { phase: u64 },
52    #[error("a study must contain at least one phase")]
53    EmptyPhaseSet,
54    #[error("phase {phase} must have a nonempty label")]
55    InvalidPhaseLabel { phase: u64 },
56    #[error("phase {phase} max_active_tasks must be greater than zero")]
57    InvalidPhaseWorkloadLimit { phase: u64 },
58    #[error("phase {phase} prepared_task_queue_capacity must be greater than zero")]
59    InvalidPhaseQueueCapacity { phase: u64 },
60    #[error("phase {phase} timing setting `{setting}` must be nonzero and representable")]
61    InvalidPhaseTiming { phase: u64, setting: &'static str },
62    #[error("phase ID {phase} appears more than once")]
63    DuplicatePhaseId { phase: u64 },
64    #[error("phase {phase} depends on unknown phase {dependency}")]
65    UnknownPhaseDependency { phase: u64, dependency: u64 },
66    #[error("phase dependency graph contains a cycle involving phase {phase}")]
67    PhaseDependencyCycle { phase: u64 },
68    #[error("selected phase {phase} is not registered")]
69    UnknownSelectedPhase { phase: u64 },
70    #[error("selected phase {phase} requires unsatisfied phase {dependency}")]
71    UnsatisfiedPhaseDependency { phase: u64, dependency: u64 },
72    #[error("confirmation input ended after phase {phase} before the next phase could start")]
73    PhaseConfirmationEof { phase: u64 },
74    #[error("failed to read confirmation after phase {phase}")]
75    PhaseConfirmationInput {
76        phase: u64,
77        #[source]
78        source: io::Error,
79    },
80    #[error("task `{task}` has no workload")]
81    MissingTaskWorkload { task: String },
82    #[error("task `{task}` failed: {source}")]
83    TaskWorkload {
84        task: String,
85        #[source]
86        source: Box<dyn std::error::Error + Send + Sync + 'static>,
87    },
88    #[error("task `{task}` exceeded its timeout of {timeout:?}")]
89    TaskTimedOut { task: String, timeout: Duration },
90    #[error("phase {phase} exceeded its deadline of {deadline_after:?}")]
91    PhaseDeadlineExceeded {
92        phase: u64,
93        deadline_after: Duration,
94    },
95    #[error("a study scheduler worker panicked")]
96    SchedulerPanicked,
97    #[error("study execution was cancelled")]
98    Cancelled,
99    #[error("phase {phase} contains an empty task ID")]
100    InvalidTaskId { phase: u64 },
101    #[error("task `{task}` must have a nonempty category")]
102    InvalidTaskCategory { task: String },
103    #[error("phase {phase} repeats task ID `{task}`")]
104    DuplicateTaskId { phase: u64, task: String },
105    #[error("task selector `{selector}` matched no task")]
106    TaskNotFound { selector: String },
107    #[error("task selector `{selector}` is ambiguous between `{first}` and `{second}`")]
108    TaskSelectorAmbiguous {
109        selector: String,
110        first: String,
111        second: String,
112    },
113    #[error("task `{task}` does not contain metadata `{key}`")]
114    UnknownTaskMetadata { task: String, key: String },
115    #[error("task `{task}` metadata `{key}` could not be decoded")]
116    DecodeTaskMetadata {
117        task: String,
118        key: String,
119        #[source]
120        source: serde_json::Error,
121    },
122    #[error("task `{task}` is not registered with this renderer")]
123    UnknownTask { task: String },
124    #[error("task `{task}` is declared as {actual}, not {requested}")]
125    TaskModeMismatch {
126        task: String,
127        requested: &'static str,
128        actual: &'static str,
129    },
130    #[error("task `{identity}` has already started or reached a terminal status")]
131    TaskAlreadyStarted { identity: String },
132    #[error("task `{identity}` starts at iteration {initial}, beyond target {target}")]
133    InitialIterationBeyondTarget {
134        identity: String,
135        initial: u64,
136        target: u64,
137    },
138    #[error("task `{identity}` cannot move progress from iteration {current} back to {attempted}")]
139    IterationRegressed {
140        identity: String,
141        current: u64,
142        attempted: u64,
143    },
144    #[error("task `{identity}` reported iteration {iteration}, beyond target {target}")]
145    IterationBeyondTarget {
146        identity: String,
147        iteration: u64,
148        target: u64,
149    },
150    #[error("task `{identity}` completed at iteration {current}, before target {target}")]
151    TargetIterationNotReached {
152        identity: String,
153        current: u64,
154        target: u64,
155    },
156    #[error("failed to start the study renderer")]
157    StartRenderer {
158        #[source]
159        source: io::Error,
160    },
161    #[error("failed to {operation} for the study display")]
162    TerminalSetup {
163        operation: &'static str,
164        #[source]
165        source: io::Error,
166    },
167    #[error("the study renderer is no longer available")]
168    RendererUnavailable,
169    #[error("the study renderer panicked")]
170    RendererPanicked,
171    #[error(
172        "cannot report success with {pending} pending, {running} running, and {failed} failed tasks"
173    )]
174    IncompleteProgress {
175        pending: u64,
176        running: u64,
177        failed: u64,
178    },
179}
180
181impl StudyError {
182    pub fn study_summary(&self) -> Option<&super::StudySummary> {
183        match self {
184            Self::PhaseExecutionFailed { summary, .. } => Some(summary),
185            _ => None,
186        }
187    }
188
189    pub fn execution_cause(&self) -> Option<&StudyError> {
190        match self {
191            Self::PhaseExecutionFailed { source, .. } => Some(source),
192            _ => None,
193        }
194    }
195}