Skip to main content

stackless_core/engine/
error.rs

1//! Engine errors. Substrate failures pass their own stable code
2//! through; the engine adds the step and instance context the §2
3//! contract requires.
4
5use crate::def::DefError;
6use crate::fault::{ErrorContext, Fault, codes};
7use crate::state::StateError;
8use crate::substrate::SubstrateFault;
9
10#[derive(Debug, thiserror::Error)]
11pub enum EngineError {
12    #[error(transparent)]
13    Def(#[from] DefError),
14
15    #[error(transparent)]
16    State(#[from] StateError),
17
18    #[error("instance {instance:?} lives on substrate {existing:?}, not {requested:?}")]
19    SubstrateMismatch {
20        instance: String,
21        existing: String,
22        requested: String,
23    },
24
25    #[error("substrate {substrate:?} does not run pinned checkouts (--source)")]
26    SourceOverrideUnsupported { substrate: String },
27
28    #[error(
29        "service {service:?} --source path {path:?} is already pinned by active instance {other:?}"
30    )]
31    SourceOverrideShared {
32        instance: String,
33        service: String,
34        path: String,
35        other: String,
36    },
37
38    #[error("step {step} on instance {instance:?} failed: {fault}")]
39    Step {
40        instance: String,
41        step: String,
42        fault: SubstrateFault,
43    },
44
45    #[error("substrate {substrate} rejected the definition: {fault}")]
46    SubstrateValidation {
47        substrate: String,
48        fault: SubstrateFault,
49    },
50
51    #[error("teardown of {instance:?} left survivors: {survivors:?}")]
52    TeardownSurvivors {
53        instance: String,
54        survivors: Vec<String>,
55    },
56}
57
58impl Fault for EngineError {
59    fn code(&self) -> &'static str {
60        match self {
61            Self::Def(err) => err.code(),
62            Self::State(err) => err.code(),
63            Self::SubstrateMismatch { .. } => codes::ENGINE_SUBSTRATE_MISMATCH,
64            Self::SourceOverrideUnsupported { .. } => codes::ENGINE_SOURCE_OVERRIDE_UNSUPPORTED,
65            Self::SourceOverrideShared { .. } => codes::ENGINE_SOURCE_OVERRIDE_SHARED,
66            // The substrate's own code is the meaningful one; the
67            // engine only adds context.
68            Self::Step { fault, .. } | Self::SubstrateValidation { fault, .. } => fault.code,
69            Self::TeardownSurvivors { .. } => codes::ENGINE_TEARDOWN_SURVIVORS,
70        }
71    }
72
73    fn remediation(&self) -> String {
74        match self {
75            Self::Def(err) => err.remediation(),
76            Self::State(err) => err.remediation(),
77            Self::SubstrateMismatch {
78                instance, existing, ..
79            } => format!(
80                "the substrate is chosen at creation only: operate on {instance:?} without \
81                 --on (it resolves to {existing:?}), or pick a new name for a {existing:?}-free \
82                 instance"
83            ),
84            Self::SourceOverrideUnsupported { .. } => {
85                "commit and push, then pin the ref in stackless.toml — cloud substrates deploy \
86                 committed refs only"
87                    .into()
88            }
89            Self::SourceOverrideShared { other, .. } => format!(
90                "use a separate git worktree per parallel agent, omit --source so stackless \
91                 materializes per-instance checkouts, or `stackless down {other}` before reusing \
92                 this checkout"
93            ),
94            Self::Step { fault, .. } | Self::SubstrateValidation { fault, .. } => {
95                fault.remediation.clone()
96            }
97            Self::TeardownSurvivors { survivors, .. } => format!(
98                "these resources still exist and may bill or hold state: {survivors:?}; re-run \
99                 `stackless down` to retry, or remove them on the substrate and re-run to verify"
100            ),
101        }
102    }
103
104    fn step(&self) -> Option<&str> {
105        match self {
106            Self::Step { step, .. } => Some(step),
107            _ => None,
108        }
109    }
110
111    fn instance(&self) -> Option<&str> {
112        match self {
113            Self::State(err) => err.instance(),
114            Self::SubstrateMismatch { instance, .. }
115            | Self::SourceOverrideShared { instance, .. }
116            | Self::Step { instance, .. }
117            | Self::TeardownSurvivors { instance, .. } => Some(instance),
118            _ => None,
119        }
120    }
121
122    fn context(&self) -> ErrorContext {
123        match self {
124            Self::Step { fault, .. } | Self::SubstrateValidation { fault, .. } => {
125                fault.context.as_ref().clone()
126            }
127            _ => ErrorContext::default(),
128        }
129    }
130}