reifydb_runtime/fatal/
mod.rs1pub 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 assert!(FatalConfig::default().armed);
188 }
189
190 #[test]
191 fn the_env_override_only_accepts_known_spellings() {
192 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 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 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 let payload: Box<dyn Any + Send> = Box::new(42u32);
219
220 assert_eq!(describe_payload(&payload), "<non-string panic payload>");
221 }
222}