crates/kernel/ents-effect/src/error.rs
error.rshistorycomment on this file
| 1 | //! `ents-effect`'s error type: everything that can prevent a run from |
| 2 | //! reaching a recorded outcome. |
| 3 | //! |
| 4 | //! Mirrors `ents-receive`'s split: an [`Error`] means the run never reached |
| 5 | //! a judgment at all (a store or object read failed, a sandbox never |
| 6 | //! started, `curl`/`tar`/`docker`/`sprite` was not on `PATH`) — as opposed |
| 7 | //! to a completed run reporting `pass` or `fail` |
| 8 | //! (`effect.result-taxonomy`), which is a reached judgment, not an `Err`. |
| 9 | //! Per `effect.result-taxonomy`, an [`Error`] here is exactly the "queue |
| 10 | //! concern with bounded retry" case: this crate never turns one into a |
| 11 | //! `Status::Error` result itself — only a caller that has exhausted its own |
| 12 | //! retry bound does that, by calling [`crate::write_result`] with |
| 13 | //! `Status::Error` explicitly. |
| 14 | |
| 15 | use std::path::PathBuf; |
| 16 | |
| 17 | use gix_hash::ObjectId; |
| 18 | |
| 19 | /// Everything that can prevent an `ents-effect` operation from reaching a |
| 20 | /// result. |
| 21 | #[derive(Debug, thiserror::Error)] |
| 22 | pub enum Error { |
| 23 | /// The ref store's read or write half failed. |
| 24 | #[error("ref store operation failed: {0}")] |
| 25 | Refs(#[from] gix_ref_store::Error), |
| 26 | |
| 27 | /// `receive` (the write-back path, `effect.results-writeback`) could |
| 28 | /// not reach an outcome. |
| 29 | /// |
| 30 | /// Boxed rather than `#[from]`-derived inline (see [`From`] below): |
| 31 | /// `ents_receive::Error` embeds `ents_gate::Error`, which is large |
| 32 | /// enough on its own to trip `clippy::result_large_err` for every |
| 33 | /// fallible function in this crate if stored inline. |
| 34 | #[error("receive failed: {0}")] |
| 35 | Receive(Box<ents_receive::Error>), |
| 36 | |
| 37 | /// The query evaluator could not compute an effect's work set. |
| 38 | #[error("query evaluation failed: {0}")] |
| 39 | Eval(#[from] ents_query::EvalError), |
| 40 | |
| 41 | /// An effect's `trigger` failed to parse as a `CommitQuery` |
| 42 | /// (`effect.validation`). |
| 43 | #[error("trigger does not parse as a CommitQuery: {0}")] |
| 44 | Trigger(#[from] ents_query::ParseError), |
| 45 | |
| 46 | /// A typed-tree entity (a [`ents_model::Status`]) could not be |
| 47 | /// (de)serialized. |
| 48 | #[error("typed-tree operation failed: {0}")] |
| 49 | Facet(#[from] facet_git_tree::Error), |
| 50 | |
| 51 | /// An object could not be read or decoded. |
| 52 | #[error("object {oid} could not be read: {detail}")] |
| 53 | Decode { |
| 54 | /// The undecodable object. |
| 55 | oid: ObjectId, |
| 56 | /// What failed, human-readable. |
| 57 | detail: String, |
| 58 | }, |
| 59 | |
| 60 | /// An object referenced by a tree or commit is missing from the object |
| 61 | /// store. |
| 62 | #[error("object {oid} is missing")] |
| 63 | Missing { |
| 64 | /// The missing object. |
| 65 | oid: ObjectId, |
| 66 | }, |
| 67 | |
| 68 | /// `refs/meta/toolchains/<name>` does not exist. |
| 69 | #[error("no toolchain named {0:?}")] |
| 70 | UnknownToolchain(String), |
| 71 | |
| 72 | /// The named entity (a toolchain, looked up by something other than |
| 73 | /// its recipe — e.g. `ents-kiln`'s `toolchain::log`) does not exist. |
| 74 | /// Distinct from [`Error::UnknownToolchain`], which names the |
| 75 | /// resolve-a-recipe failure specifically; this variant is the general |
| 76 | /// "nothing at that name" case a caller renders as a not-found error. |
| 77 | #[error("not found: {what}")] |
| 78 | NotFound { |
| 79 | /// What was being looked up. |
| 80 | what: String, |
| 81 | }, |
| 82 | |
| 83 | /// A toolchain's `recipe` field did not parse as a `Recipe` |
| 84 | /// (`ents-kiln`'s own type; `effect.toolchains`: "a manifest's declared |
| 85 | /// components MUST be resolved during effect execution"). |
| 86 | #[error("toolchain {name:?} has an unreadable recipe: {detail}")] |
| 87 | InvalidRecipe { |
| 88 | /// The toolchain's name. |
| 89 | name: String, |
| 90 | /// What failed, human-readable. |
| 91 | detail: String, |
| 92 | }, |
| 93 | |
| 94 | /// An effect's `toolchains` list named something that is not a valid |
| 95 | /// ref-path segment (`effect.validation`). |
| 96 | #[error("{0:?} is not a valid toolchain name")] |
| 97 | InvalidToolchainName(String), |
| 98 | |
| 99 | /// A materialized tree entry was a git submodule (a commit entry). |
| 100 | /// Gitlinks retain nothing in this design (no embedded submodule |
| 101 | /// content), so materializing one is refused rather than silently |
| 102 | /// skipped. |
| 103 | #[error("cannot materialize {path:?}: it is a git submodule (gitlink)")] |
| 104 | Submodule { |
| 105 | /// The offending path, relative to the materialization root. |
| 106 | path: String, |
| 107 | }, |
| 108 | |
| 109 | /// A tree entry's filename, or a downloaded component's extracted file |
| 110 | /// name, was not valid UTF-8. |
| 111 | #[error("{0:?} is not valid UTF-8")] |
| 112 | NotUtf8(PathBuf), |
| 113 | |
| 114 | /// A tree entry carried a name that could escape or collide inside the |
| 115 | /// materialization destination — `.`, `..`, a path separator, or a |
| 116 | /// duplicate of an earlier entry in the same tree. Checkout runs on |
| 117 | /// the *host*, before any sandbox exists, so a crafted (fsck-invalid |
| 118 | /// but storable) tree is refused before anything is written. |
| 119 | #[error("refusing to materialize tree entry {name:?}: {detail}")] |
| 120 | UnsafeEntry { |
| 121 | /// The offending entry name. |
| 122 | name: String, |
| 123 | /// Why it was refused, human-readable. |
| 124 | detail: String, |
| 125 | }, |
| 126 | |
| 127 | /// A path under a materialization destination could not be read or |
| 128 | /// written. |
| 129 | #[error("could not access {path}: {source}")] |
| 130 | Io { |
| 131 | /// The path being accessed. |
| 132 | path: PathBuf, |
| 133 | /// The underlying I/O error. |
| 134 | #[source] |
| 135 | source: std::io::Error, |
| 136 | }, |
| 137 | |
| 138 | /// A `Component` (`ents-kiln`'s own type) carried a `dest` that is not |
| 139 | /// empty or a single safe path segment, a `url` that is empty or |
| 140 | /// carries whitespace or a quote, or a `sha256` that is not 64 hex |
| 141 | /// characters — all of which feed filesystem paths, `curl`, or an |
| 142 | /// in-sandbox shell string downstream. Checked when a recipe is parsed |
| 143 | /// and re-checked before any fetch. |
| 144 | #[error("invalid toolchain component: {0}")] |
| 145 | InvalidComponent(String), |
| 146 | |
| 147 | /// Running an external program (`docker`, `sprite`, `curl`, `tar`, |
| 148 | /// `sha256sum`/`shasum`) failed to start at all — the readiness probes |
| 149 | /// this phase ports from `pre-redo` exist precisely to turn this into |
| 150 | /// an actionable message instead of a raw "os error 2". |
| 151 | #[error("could not run `{program}`: {detail}")] |
| 152 | Spawn { |
| 153 | /// The program that could not be started. |
| 154 | program: String, |
| 155 | /// What failed, human-readable. |
| 156 | detail: String, |
| 157 | }, |
| 158 | |
| 159 | /// An external program ran but reported failure (nonzero exit, or |
| 160 | /// output this crate could not parse). |
| 161 | #[error("{program} failed: {detail}")] |
| 162 | Process { |
| 163 | /// The program that failed. |
| 164 | program: String, |
| 165 | /// What failed, human-readable. |
| 166 | detail: String, |
| 167 | }, |
| 168 | |
| 169 | /// A downloaded component's content did not match its recorded |
| 170 | /// sha256 — refused rather than extracted anyway. |
| 171 | #[error("{url}: expected sha256 {expected}, got {actual}")] |
| 172 | HashMismatch { |
| 173 | /// The component's source URL. |
| 174 | url: String, |
| 175 | /// The recorded sha256. |
| 176 | expected: String, |
| 177 | /// The sha256 actually computed. |
| 178 | actual: String, |
| 179 | }, |
| 180 | |
| 181 | /// The sandbox reported an infrastructure failure rather than a |
| 182 | /// completed run — never itself a `Status::Error` result |
| 183 | /// (`effect.result-taxonomy`); see this type's own doc. |
| 184 | #[error("the sandbox did not complete a run: {0}")] |
| 185 | Sandbox(String), |
| 186 | |
| 187 | /// Writing a new object to the object store failed (building a |
| 188 | /// toolchain import's tree, `ents-kiln`'s `toolchain::import`). |
| 189 | #[error(transparent)] |
| 190 | ObjectWrite(#[from] gix_object::write::Error), |
| 191 | |
| 192 | /// One commit's run stopped a [`crate::run_effect`] batch. Earlier |
| 193 | /// commits' outcomes were already durably recorded through `receive` |
| 194 | /// before this error, so a caller applying its own retry policy |
| 195 | /// (`effect.deployment-property`) knows exactly which commit to resume |
| 196 | /// from — and a plain retry of the whole batch re-runs only what is |
| 197 | /// still owed, since the work set subtracts recorded results |
| 198 | /// (`query.workset`). |
| 199 | #[error("run for {oid} stopped the batch: {source}")] |
| 200 | Run { |
| 201 | /// The commit whose run failed. |
| 202 | oid: ObjectId, |
| 203 | /// The underlying failure. |
| 204 | #[source] |
| 205 | source: Box<Error>, |
| 206 | }, |
| 207 | } |
| 208 | |
| 209 | impl From<ents_receive::Error> for Error { |
| 210 | fn from(source: ents_receive::Error) -> Self { |
| 211 | Self::Receive(Box::new(source)) |
| 212 | } |
| 213 | } |
| 214 | |
| 215 | /// The `Result` alias every fallible `ents-effect` operation returns. |
| 216 | pub type Result<T> = std::result::Result<T, Error>; |