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// Renders an error chain on one line for logs and text sinks. Each link prints
70// its own line once; a link whose text an outer link already embeds is
71// skipped, so a cause is never repeated.
72pub struct Report<'a>(pub &'a (dyn std::error::Error + 'static));
73
74// What the originating process observed: its source chain and, when that
75// process enabled capture, the backtrace where its carrier received the error.
76// The origin names whose frames these are, so a host capture never poses as a
77// plugin's.
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        let chain: Vec<String> = sources(error)
131            .map(|error| match error.downcast_ref::<crate::Refusal>() {
132                Some(refusal) => refusal.message().to_owned(),
133                None => error.to_string(),
134            })
135            .collect();
136        let cause = chain.iter().rev().find(|message| !message.trim().is_empty()).cloned().unwrap_or_else(|| "operation failed".to_owned());
137
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        let (failure, rest) = postcard::take_from_bytes::<Self>(rest).map_err(CodecError::Encoding)?;
194        if !rest.is_empty() {
195            return Err(CodecError::Trailing);
196        }
197        Ok(failure)
198    }
199}
200
201impl fmt::Display for Failure {
202    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
203        formatter.write_str(&self.0.cause)
204    }
205}
206
207impl std::error::Error for Failure {}
208
209impl fmt::Display for Report<'_> {
210    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
211        let mut rendered = String::new();
212        for link in sources(self.0) {
213            let line = link.to_string();
214            if line.trim().is_empty() || rendered.contains(&line) {
215                continue;
216            }
217            if !rendered.is_empty() {
218                rendered.push_str(": ");
219            }
220            rendered.push_str(&line);
221        }
222        formatter.write_str(&rendered)
223    }
224}
225
226impl Context {
227    pub fn operation(&self) -> Option<&str> {
228        self.operation.as_deref()
229    }
230
231    pub fn step(&self) -> Option<&str> {
232        self.step.as_deref()
233    }
234}
235
236impl Diagnostics {
237    pub fn origin(&self) -> Origin {
238        self.origin
239    }
240
241    pub fn chain(&self) -> &[String] {
242        &self.chain
243    }
244
245    pub fn backtrace(&self) -> Option<&str> {
246        self.backtrace.as_deref()
247    }
248}
249
250/* === Public Functions === */
251
252/* === Private Functions === */
253
254fn sources<'a>(error: &'a (dyn std::error::Error + 'static)) -> impl Iterator<Item = &'a (dyn std::error::Error + 'static)> {
255    std::iter::successors(Some(error), |error| error.source())
256}
257
258fn carried<'a>(error: &'a (dyn std::error::Error + 'static)) -> Option<&'a Failure> {
259    sources(error)
260        .find_map(|error| error.downcast_ref::<Failure>().or_else(|| error.downcast_ref::<crate::Refusal>().map(crate::Refusal::failure)))
261}
262
263fn bounded(mut text: String, limit: usize) -> String {
264    if text.len() <= limit {
265        return text;
266    }
267    let mut end = limit;
268    while !text.is_char_boundary(end) {
269        end -= 1;
270    }
271    text.truncate(end);
272    text
273}
274
275/* === Tests === */