Skip to main content

harmos_domain/
failure.rs

1/* ***
2 *
3 * Harmos
4 *
5 * SPDX-License-Identifier: Apache-2.0
6 *
7 * ***
8 */
9
10// One operation failure as every carrier transports it. The cause is written
11// once where the error originates, the operation frame is added once where a
12// call crosses from a plugin into the host, and the step frame belongs to the
13// consumer. Sidecar frames, host refusals, gRPC status details and guest
14// records carry the same value; no carrier re-embeds another's text.
15
16/* === Modules === */
17
18/* === Re-Exports === */
19
20/* === Imports === */
21
22// Standard Library Imports
23use std::backtrace::{Backtrace, BacktraceStatus};
24use std::fmt;
25// Third Party Imports
26use serde::{Deserialize, Serialize};
27// Local Imports
28use crate::operations;
29
30/* === Constants === */
31
32const LINK_LIMIT: usize = 4 * 1024;
33
34const CHAIN_LIMIT: usize = 16;
35
36const BACKTRACE_LIMIT: usize = 32 * 1024;
37
38const VERSION: u8 = 1;
39
40/* === Macros === */
41
42/* === Type Aliases === */
43
44/* === Structs === */
45
46// Boxed so every error enum that carries a failure stays one pointer wide.
47#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(transparent)]
49pub struct Failure(Box<Record>);
50
51#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(deny_unknown_fields)]
53struct Record {
54    code: operations::FailureKind,
55    cause: String,
56    context: Context,
57    diagnostics: Option<Diagnostics>,
58}
59
60// The frames that locate a failure, one per request hop. Each is set once by
61// the boundary that owns it, so a frame never repeats.
62#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(deny_unknown_fields)]
64pub struct Context {
65    operation: Option<String>,
66    step: Option<String>,
67}
68
69// What the originating process observed: its source chain and, when that
70// process enabled capture, the backtrace where its carrier received the error.
71// The origin names whose frames these are, so a host capture never poses as a
72// plugin's.
73// Renders an error chain on one line for logs and text sinks. Each link prints
74// its own line once; a link whose text an outer link already embeds is
75// skipped, so a cause is never repeated.
76pub struct Report<'a>(pub &'a (dyn std::error::Error + 'static));
77
78#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
79#[serde(deny_unknown_fields)]
80pub struct Diagnostics {
81    origin: Origin,
82    chain: Vec<String>,
83    backtrace: Option<String>,
84}
85
86/* === Enums === */
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
89#[serde(rename_all = "snake_case")]
90pub enum Origin {
91    Plugin,
92    Host,
93}
94
95#[derive(Debug, thiserror::Error)]
96pub enum CodecError {
97    #[error("failure encoding version is unsupported")]
98    Version,
99
100    #[error("failure encoding has trailing bytes")]
101    Trailing,
102
103    #[error("failure encoding is malformed")]
104    Encoding(#[source] postcard::Error),
105}
106
107/* === Traits === */
108
109/* === Implementations === */
110
111impl Failure {
112    pub fn new(code: operations::FailureKind, cause: impl Into<String>) -> Self {
113        Self(Box::new(Record { code, cause: cause.into(), context: Context::default(), diagnostics: None }))
114    }
115
116    // Writes the cause once from an error chain. A failure already carried in
117    // the chain keeps its cause, frames and diagnostics, taking only the
118    // classification of the handler that returned it; otherwise the deepest
119    // meaningful message becomes the cause and the whole chain moves into
120    // diagnostics.
121    pub fn originate(code: operations::FailureKind, error: &(dyn std::error::Error + 'static), origin: Origin) -> Self {
122        if let Some(carried) = carried(error) {
123            let mut forwarded = carried.clone();
124            forwarded.0.code = code;
125            return forwarded;
126        }
127
128        // Collapse The Chain To Its Deepest Meaningful Message
129        let chain: Vec<String> = sources(error)
130            .map(|error| match error.downcast_ref::<crate::Refusal>() {
131                Some(refusal) => refusal.message().to_owned(),
132                None => error.to_string(),
133            })
134            .collect();
135        let cause = chain.iter().rev().find(|message| !message.trim().is_empty()).cloned().unwrap_or_else(|| "operation failed".to_owned());
136
137        // Keep The Full Chain And Any Capture As Origin Diagnostics
138        let backtrace = Backtrace::capture();
139        let backtrace = (backtrace.status() == BacktraceStatus::Captured).then(|| bounded(backtrace.to_string(), BACKTRACE_LIMIT));
140        let chain = chain.into_iter().take(CHAIN_LIMIT).map(|message| bounded(message, LINK_LIMIT)).collect();
141        let diagnostics = Diagnostics { origin, chain, backtrace };
142
143        let mut failure = Self::new(code, cause);
144        failure.0.diagnostics = Some(diagnostics);
145        failure
146    }
147
148    // The failure an error chain carries, if any link is or holds one.
149    pub fn find<'a>(error: &'a (dyn std::error::Error + 'static)) -> Option<&'a Self> {
150        carried(error)
151    }
152
153    pub fn code(&self) -> operations::FailureKind {
154        self.0.code
155    }
156
157    pub fn cause(&self) -> &str {
158        &self.0.cause
159    }
160
161    pub fn context(&self) -> &Context {
162        &self.0.context
163    }
164
165    pub fn diagnostics(&self) -> Option<&Diagnostics> {
166        self.0.diagnostics.as_ref()
167    }
168
169    // The host adds the operation frame where a call crosses from a plugin.
170    // The frame closest to the origin wins, so a forwarded failure keeps it.
171    pub fn in_operation(mut self, operation: impl Into<String>) -> Self {
172        self.0.context.operation.get_or_insert_with(|| operation.into());
173        self
174    }
175
176    // The consumer adds the step frame that issued the request.
177    pub fn in_step(mut self, step: impl Into<String>) -> Self {
178        self.0.context.step.get_or_insert_with(|| step.into());
179        self
180    }
181
182    pub fn without_diagnostics(mut self) -> Self {
183        self.0.diagnostics = None;
184        self
185    }
186
187    pub fn encode(&self) -> Vec<u8> {
188        postcard::to_allocvec(&(VERSION, self)).expect("a failure serializes to postcard")
189    }
190
191    pub fn decode(bytes: &[u8]) -> Result<Self, CodecError> {
192        let (version, rest) = postcard::take_from_bytes::<u8>(bytes).map_err(CodecError::Encoding)?;
193        if version != VERSION {
194            return Err(CodecError::Version);
195        }
196        let (failure, rest) = postcard::take_from_bytes::<Self>(rest).map_err(CodecError::Encoding)?;
197        if !rest.is_empty() {
198            return Err(CodecError::Trailing);
199        }
200        Ok(failure)
201    }
202}
203
204impl fmt::Display for Failure {
205    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
206        formatter.write_str(&self.0.cause)
207    }
208}
209
210impl std::error::Error for Failure {}
211
212impl fmt::Display for Report<'_> {
213    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
214        let mut rendered = String::new();
215        for link in sources(self.0) {
216            let line = link.to_string();
217            if line.trim().is_empty() || rendered.contains(&line) {
218                continue;
219            }
220            if !rendered.is_empty() {
221                rendered.push_str(": ");
222            }
223            rendered.push_str(&line);
224        }
225        formatter.write_str(&rendered)
226    }
227}
228
229impl Context {
230    pub fn operation(&self) -> Option<&str> {
231        self.operation.as_deref()
232    }
233
234    pub fn step(&self) -> Option<&str> {
235        self.step.as_deref()
236    }
237}
238
239impl Diagnostics {
240    pub fn origin(&self) -> Origin {
241        self.origin
242    }
243
244    pub fn chain(&self) -> &[String] {
245        &self.chain
246    }
247
248    pub fn backtrace(&self) -> Option<&str> {
249        self.backtrace.as_deref()
250    }
251}
252
253/* === Public Functions === */
254
255/* === Private Functions === */
256
257fn sources<'a>(error: &'a (dyn std::error::Error + 'static)) -> impl Iterator<Item = &'a (dyn std::error::Error + 'static)> {
258    std::iter::successors(Some(error), |error| error.source())
259}
260
261fn carried<'a>(error: &'a (dyn std::error::Error + 'static)) -> Option<&'a Failure> {
262    sources(error)
263        .find_map(|error| error.downcast_ref::<Failure>().or_else(|| error.downcast_ref::<crate::Refusal>().map(crate::Refusal::failure)))
264}
265
266fn bounded(mut text: String, limit: usize) -> String {
267    if text.len() <= limit {
268        return text;
269    }
270    let mut end = limit;
271    while !text.is_char_boundary(end) {
272        end -= 1;
273    }
274    text.truncate(end);
275    text
276}
277
278/* === Tests === */