Skip to main content

reifydb_runtime/fatal/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4pub mod report;
5
6use std::{
7	any::Any,
8	backtrace::Backtrace,
9	env,
10	io::{self, Write},
11	panic::{self, PanicHookInfo},
12	process,
13	sync::{
14		Once,
15		atomic::{AtomicBool, Ordering},
16	},
17};
18
19use tracing::error;
20
21use crate::fatal::report::{FatalKind, FatalReport, Origin};
22
23static ARMED: AtomicBool = AtomicBool::new(true);
24static INSTALLED: Once = Once::new();
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct FatalConfig {
28	pub armed: bool,
29}
30
31impl Default for FatalConfig {
32	fn default() -> Self {
33		Self {
34			armed: env_armed().unwrap_or(true),
35		}
36	}
37}
38
39impl FatalConfig {
40	pub fn armed(armed: bool) -> Self {
41		Self {
42			armed,
43		}
44	}
45
46	pub fn disarmed() -> Self {
47		Self::armed(false)
48	}
49}
50
51fn env_armed() -> Option<bool> {
52	match env::var("REIFYDB_FATAL").ok()?.as_str() {
53		"0" | "off" | "false" => Some(false),
54		"1" | "on" | "true" => Some(true),
55		_ => None,
56	}
57}
58
59pub fn install(config: FatalConfig) {
60	ARMED.store(config.armed, Ordering::Release);
61	INSTALLED.call_once(|| {
62		panic::set_hook(Box::new(on_panic));
63	});
64}
65
66pub fn arm() {
67	ARMED.store(true, Ordering::Release);
68}
69
70pub fn disarm() {
71	ARMED.store(false, Ordering::Release);
72}
73
74pub fn is_armed() -> bool {
75	ARMED.load(Ordering::Acquire)
76}
77
78pub fn emit(report: &FatalReport) {
79	let rendered = report.render();
80	error!(
81		fatal.id = %report.error_id(),
82		fatal.kind = report.kind.as_str(),
83		fatal.thread = %report.thread_name,
84		"{}",
85		report.reason
86	);
87	let mut err = io::stderr().lock();
88	let _ = writeln!(err, "{}", rendered);
89	let _ = err.flush();
90}
91
92pub fn fatal(report: FatalReport) -> ! {
93	emit(&report);
94	process::abort()
95}
96
97fn on_panic(info: &PanicHookInfo<'_>) {
98	let backtrace = Backtrace::force_capture();
99	let mut report = FatalReport::new(FatalKind::Panic, panic_message(info));
100	if let Some(location) = info.location() {
101		report = report.origin(Origin::new(location.file(), location.line(), location.column()));
102	}
103	if !is_armed() {
104		emit(&report.backtrace(backtrace.to_string()));
105		return;
106	}
107	fatal(report.backtrace(backtrace.to_string()))
108}
109
110pub fn panic_message(info: &PanicHookInfo<'_>) -> String {
111	let payload = info.payload();
112	if let Some(message) = payload.downcast_ref::<&'static str>() {
113		(*message).to_string()
114	} else if let Some(message) = payload.downcast_ref::<String>() {
115		message.clone()
116	} else {
117		"<non-string panic payload>".to_string()
118	}
119}
120
121pub fn describe_payload(payload: &Box<dyn Any + Send>) -> String {
122	if let Some(message) = payload.downcast_ref::<&'static str>() {
123		(*message).to_string()
124	} else if let Some(message) = payload.downcast_ref::<String>() {
125		message.clone()
126	} else {
127		"<non-string panic payload>".to_string()
128	}
129}
130
131#[macro_export]
132macro_rules! fatal {
133	($reason:expr) => {
134		$crate::fatal::fatal(
135			$crate::fatal::report::FatalReport::new($crate::fatal::report::FatalKind::Invariant, $reason)
136				.origin($crate::fatal::report::Origin::new(file!(), line!(), column!()))
137				.backtrace(std::backtrace::Backtrace::force_capture().to_string()),
138		)
139	};
140	($fmt:expr, $($arg:tt)*) => {
141		$crate::fatal::fatal(
142			$crate::fatal::report::FatalReport::new($crate::fatal::report::FatalKind::Invariant, format!($fmt, $($arg)*))
143				.origin($crate::fatal::report::Origin::new(file!(), line!(), column!()))
144				.backtrace(std::backtrace::Backtrace::force_capture().to_string()),
145		)
146	};
147}
148
149#[macro_export]
150macro_rules! fatal_on_err {
151	($expr:expr) => {
152		match $expr {
153			Ok(value) => value,
154			Err(err) => $crate::fatal::fatal(
155				$crate::fatal::report::FatalReport::new(
156					$crate::fatal::report::FatalKind::Error,
157					format!("{:?}", err),
158				)
159				.origin($crate::fatal::report::Origin::new(file!(), line!(), column!()))
160				.backtrace(std::backtrace::Backtrace::force_capture().to_string()),
161			),
162		}
163	};
164	($expr:expr, $component:expr) => {
165		match $expr {
166			Ok(value) => value,
167			Err(err) => $crate::fatal::fatal(
168				$crate::fatal::report::FatalReport::new(
169					$crate::fatal::report::FatalKind::Error,
170					format!("{:?}", err),
171				)
172				.component($component)
173				.origin($crate::fatal::report::Origin::new(file!(), line!(), column!()))
174				.backtrace(std::backtrace::Backtrace::force_capture().to_string()),
175			),
176		}
177	};
178}
179
180#[cfg(test)]
181mod tests {
182	use super::*;
183
184	#[test]
185	fn the_default_config_is_armed_because_a_forgotten_flag_must_not_reopen_the_hole() {
186		// A default of off would leave a deployment that never sets the flag swallowing exactly like before.
187		assert!(FatalConfig::default().armed);
188	}
189
190	#[test]
191	fn the_env_override_only_accepts_known_spellings() {
192		// An unparsed value must fall through to the armed default rather than silently disarming.
193		assert_eq!(FatalConfig::disarmed().armed, false);
194		assert!(FatalConfig::armed(true).armed);
195	}
196
197	#[test]
198	fn a_string_panic_payload_survives_into_the_reason() {
199		// Most sites today drop the payload with `|_|`, which is what makes their logs useless.
200		let payload: Box<dyn Any + Send> = Box::new("boom".to_string());
201
202		assert_eq!(describe_payload(&payload), "boom");
203	}
204
205	#[test]
206	fn a_static_str_panic_payload_survives_into_the_reason() {
207		// `panic!("literal")` produces &'static str, not String, and downcasting only one of the two loses half
208		// the panics.
209		let payload: Box<dyn Any + Send> = Box::new("boom");
210
211		assert_eq!(describe_payload(&payload), "boom");
212	}
213
214	#[test]
215	fn an_opaque_panic_payload_is_named_rather_than_rendered_empty() {
216		// panic_any with a custom type lands here, and an empty reason reads as "no reason" instead of
217		// "unprintable".
218		let payload: Box<dyn Any + Send> = Box::new(42u32);
219
220		assert_eq!(describe_payload(&payload), "<non-string panic payload>");
221	}
222}