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 has trailing bytes")]
98    Trailing,
99
100    #[error("failure encoding is malformed")]
101    Encoding(#[source] postcard::Error),
102}
103
104/* === Traits === */
105
106/* === Implementations === */
107
108impl Failure {
109    pub fn new(code: operations::FailureKind, cause: impl Into<String>) -> Self {
110        Self(Box::new(Record {
111            code,
112            cause: cause.into(),
113            context: Context::default(),
114            diagnostics: None,
115        }))
116    }
117
118    // Writes the cause once from an error chain. A failure already carried in
119    // the chain keeps its cause, frames and diagnostics, taking only the
120    // classification of the handler that returned it; otherwise the deepest
121    // meaningful message becomes the cause and the whole chain moves into
122    // diagnostics.
123    pub fn originate(code: operations::FailureKind, error: &(dyn std::error::Error + 'static), origin: Origin) -> Self {
124        if let Some(carried) = carried(error) {
125            let mut forwarded = carried.clone();
126            forwarded.0.code = code;
127            return forwarded;
128        }
129
130        // Collapse The Chain To Its Deepest Meaningful Message
131        let chain: Vec<String> = sources(error)
132            .map(|error| match error.downcast_ref::<crate::Refusal>() {
133                Some(refusal) => refusal.message().to_owned(),
134                None => error.to_string(),
135            })
136            .collect();
137        let cause = chain.iter().rev().find(|message| !message.trim().is_empty()).cloned().unwrap_or_else(|| "operation failed".to_owned());
138
139        // Keep The Full Chain And Any Capture As Origin Diagnostics
140        let backtrace = Backtrace::capture();
141        let backtrace = (backtrace.status() == BacktraceStatus::Captured).then(|| bounded(backtrace.to_string(), BACKTRACE_LIMIT));
142        let chain = chain.into_iter().take(CHAIN_LIMIT).map(|message| bounded(message, LINK_LIMIT)).collect();
143        let diagnostics = Diagnostics { origin, chain, backtrace };
144
145        let mut failure = Self::new(code, cause);
146        failure.0.diagnostics = Some(diagnostics);
147        failure
148    }
149
150    // The failure an error chain carries, if any link is or holds one.
151    pub fn find<'a>(error: &'a (dyn std::error::Error + 'static)) -> Option<&'a Self> {
152        carried(error)
153    }
154
155    pub fn code(&self) -> operations::FailureKind {
156        self.0.code
157    }
158
159    pub fn cause(&self) -> &str {
160        &self.0.cause
161    }
162
163    pub fn context(&self) -> &Context {
164        &self.0.context
165    }
166
167    pub fn diagnostics(&self) -> Option<&Diagnostics> {
168        self.0.diagnostics.as_ref()
169    }
170
171    // The host adds the operation frame where a call crosses from a plugin.
172    // The frame closest to the origin wins, so a forwarded failure keeps it.
173    pub fn in_operation(mut self, operation: impl Into<String>) -> Self {
174        self.0.context.operation.get_or_insert_with(|| operation.into());
175        self
176    }
177
178    // The consumer adds the step frame that issued the request.
179    pub fn in_step(mut self, step: impl Into<String>) -> Self {
180        self.0.context.step.get_or_insert_with(|| step.into());
181        self
182    }
183
184    pub fn without_diagnostics(mut self) -> Self {
185        self.0.diagnostics = None;
186        self
187    }
188
189    pub fn encode(&self) -> Vec<u8> {
190        postcard::to_allocvec(&(VERSION, self)).expect("a failure serializes to postcard")
191    }
192
193    pub fn decode(bytes: &[u8]) -> Result<Self, CodecError> {
194        let (_version, rest) = postcard::take_from_bytes::<u8>(bytes).map_err(CodecError::Encoding)?;
195        let (failure, rest) = postcard::take_from_bytes::<Self>(rest).map_err(CodecError::Encoding)?;
196        if !rest.is_empty() {
197            return Err(CodecError::Trailing);
198        }
199        Ok(failure)
200    }
201}
202
203impl fmt::Display for Failure {
204    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
205        formatter.write_str(&self.0.cause)
206    }
207}
208
209impl std::error::Error for Failure {}
210
211impl fmt::Display for Report<'_> {
212    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
213        let mut rendered = String::new();
214        for link in sources(self.0) {
215            let line = link.to_string();
216            if line.trim().is_empty() || rendered.contains(&line) {
217                continue;
218            }
219            if !rendered.is_empty() {
220                rendered.push_str(": ");
221            }
222            rendered.push_str(&line);
223        }
224        formatter.write_str(&rendered)
225    }
226}
227
228impl Context {
229    pub fn operation(&self) -> Option<&str> {
230        self.operation.as_deref()
231    }
232
233    pub fn step(&self) -> Option<&str> {
234        self.step.as_deref()
235    }
236}
237
238impl Diagnostics {
239    pub fn origin(&self) -> Origin {
240        self.origin
241    }
242
243    pub fn chain(&self) -> &[String] {
244        &self.chain
245    }
246
247    pub fn backtrace(&self) -> Option<&str> {
248        self.backtrace.as_deref()
249    }
250}
251
252/* === Public Functions === */
253
254/* === Private Functions === */
255
256fn sources<'a>(error: &'a (dyn std::error::Error + 'static)) -> impl Iterator<Item = &'a (dyn std::error::Error + 'static)> {
257    std::iter::successors(Some(error), |error| error.source())
258}
259
260fn carried<'a>(error: &'a (dyn std::error::Error + 'static)) -> Option<&'a Failure> {
261    sources(error)
262        .find_map(|error| error.downcast_ref::<Failure>().or_else(|| error.downcast_ref::<crate::Refusal>().map(crate::Refusal::failure)))
263}
264
265fn bounded(mut text: String, limit: usize) -> String {
266    if text.len() <= limit {
267        return text;
268    }
269    let mut end = limit;
270    while !text.is_char_boundary(end) {
271        end -= 1;
272    }
273    text.truncate(end);
274    text
275}
276
277/* === Tests === */