reifydb_runtime/fatal/
mod.rs1pub mod report;
17
18use std::{
19 any::Any,
20 backtrace::Backtrace,
21 env,
22 io::{self, Write},
23 panic::{self, PanicHookInfo},
24 process,
25 sync::{
26 Once,
27 atomic::{AtomicBool, Ordering},
28 },
29};
30
31use tracing::error;
32
33use crate::fatal::report::{FatalKind, FatalReport, Origin};
34
35static ARMED: AtomicBool = AtomicBool::new(true);
36static INSTALLED: Once = Once::new();
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub struct FatalConfig {
40 pub armed: bool,
41}
42
43impl Default for FatalConfig {
44 fn default() -> Self {
45 Self {
46 armed: env_armed().unwrap_or(true),
47 }
48 }
49}
50
51impl FatalConfig {
52 pub fn armed(armed: bool) -> Self {
53 Self {
54 armed,
55 }
56 }
57
58 pub fn disarmed() -> Self {
59 Self::armed(false)
60 }
61}
62
63fn env_armed() -> Option<bool> {
64 match env::var("REIFYDB_FATAL").ok()?.as_str() {
65 "0" | "off" | "false" => Some(false),
66 "1" | "on" | "true" => Some(true),
67 _ => None,
68 }
69}
70
71pub fn install(config: FatalConfig) {
72 ARMED.store(config.armed, Ordering::Release);
73 INSTALLED.call_once(|| {
74 panic::set_hook(Box::new(on_panic));
75 });
76}
77
78pub fn arm() {
79 ARMED.store(true, Ordering::Release);
80}
81
82pub fn disarm() {
83 ARMED.store(false, Ordering::Release);
84}
85
86pub fn is_armed() -> bool {
87 ARMED.load(Ordering::Acquire)
88}
89
90pub fn emit(report: &FatalReport) {
91 let rendered = report.render();
92 error!(
93 fatal.id = %report.error_id(),
94 fatal.kind = report.kind.as_str(),
95 fatal.thread = %report.thread_name,
96 "{}",
97 report.reason
98 );
99 let mut err = io::stderr().lock();
100 let _ = writeln!(err, "{}", rendered);
101 let _ = err.flush();
102}
103
104pub fn fatal(report: FatalReport) -> ! {
105 emit(&report);
106 process::abort()
107}
108
109fn on_panic(info: &PanicHookInfo<'_>) {
110 let backtrace = Backtrace::force_capture();
111 let mut report = FatalReport::new(FatalKind::Panic, panic_message(info));
112 if let Some(location) = info.location() {
113 report = report.origin(Origin::new(location.file(), location.line(), location.column()));
114 }
115 if !is_armed() {
116 emit(&report.backtrace(backtrace.to_string()));
117 return;
118 }
119 fatal(report.backtrace(backtrace.to_string()))
120}
121
122pub fn panic_message(info: &PanicHookInfo<'_>) -> String {
123 let payload = info.payload();
124 if let Some(message) = payload.downcast_ref::<&'static str>() {
125 (*message).to_string()
126 } else if let Some(message) = payload.downcast_ref::<String>() {
127 message.clone()
128 } else {
129 "<non-string panic payload>".to_string()
130 }
131}
132
133pub fn describe_payload(payload: &Box<dyn Any + Send>) -> String {
134 if let Some(message) = payload.downcast_ref::<&'static str>() {
135 (*message).to_string()
136 } else if let Some(message) = payload.downcast_ref::<String>() {
137 message.clone()
138 } else {
139 "<non-string panic payload>".to_string()
140 }
141}
142
143#[macro_export]
144macro_rules! fatal {
145 ($reason:expr) => {
146 $crate::fatal::fatal(
147 $crate::fatal::report::FatalReport::new($crate::fatal::report::FatalKind::Invariant, $reason)
148 .origin($crate::fatal::report::Origin::new(file!(), line!(), column!()))
149 .backtrace(std::backtrace::Backtrace::force_capture().to_string()),
150 )
151 };
152 ($fmt:expr, $($arg:tt)*) => {
153 $crate::fatal::fatal(
154 $crate::fatal::report::FatalReport::new($crate::fatal::report::FatalKind::Invariant, format!($fmt, $($arg)*))
155 .origin($crate::fatal::report::Origin::new(file!(), line!(), column!()))
156 .backtrace(std::backtrace::Backtrace::force_capture().to_string()),
157 )
158 };
159}
160
161#[macro_export]
162macro_rules! fatal_on_err {
163 ($expr:expr) => {
164 match $expr {
165 Ok(value) => value,
166 Err(err) => $crate::fatal::fatal(
167 $crate::fatal::report::FatalReport::new(
168 $crate::fatal::report::FatalKind::Error,
169 format!("{:?}", err),
170 )
171 .origin($crate::fatal::report::Origin::new(file!(), line!(), column!()))
172 .backtrace(std::backtrace::Backtrace::force_capture().to_string()),
173 ),
174 }
175 };
176 ($expr:expr, $component:expr) => {
177 match $expr {
178 Ok(value) => value,
179 Err(err) => $crate::fatal::fatal(
180 $crate::fatal::report::FatalReport::new(
181 $crate::fatal::report::FatalKind::Error,
182 format!("{:?}", err),
183 )
184 .component($component)
185 .origin($crate::fatal::report::Origin::new(file!(), line!(), column!()))
186 .backtrace(std::backtrace::Backtrace::force_capture().to_string()),
187 ),
188 }
189 };
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn the_default_config_is_armed_because_a_forgotten_flag_must_not_reopen_the_hole() {
198 assert!(FatalConfig::default().armed);
200 }
201
202 #[test]
203 fn the_env_override_only_accepts_known_spellings() {
204 assert_eq!(FatalConfig::disarmed().armed, false);
206 assert!(FatalConfig::armed(true).armed);
207 }
208
209 #[test]
210 fn a_string_panic_payload_survives_into_the_reason() {
211 let payload: Box<dyn Any + Send> = Box::new("boom".to_string());
213
214 assert_eq!(describe_payload(&payload), "boom");
215 }
216
217 #[test]
218 fn a_static_str_panic_payload_survives_into_the_reason() {
219 let payload: Box<dyn Any + Send> = Box::new("boom");
222
223 assert_eq!(describe_payload(&payload), "boom");
224 }
225
226 #[test]
227 fn an_opaque_panic_payload_is_named_rather_than_rendered_empty() {
228 let payload: Box<dyn Any + Send> = Box::new(42u32);
231
232 assert_eq!(describe_payload(&payload), "<non-string panic payload>");
233 }
234}