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}