hyperactor/
panic_handler.rs1use std::backtrace::Backtrace;
13use std::cell::RefCell;
14use std::future::Future;
15use std::panic;
16
17pub(crate) struct PanicInfo {
19 message: String,
21 location: Option<PanicLocation>,
23 backtrace: Backtrace,
25}
26
27impl std::fmt::Display for PanicInfo {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 write!(f, "panic at ")?;
30 match &self.location {
31 Some(loc) => write!(f, "{}", loc)?,
32 None => write!(f, "unavailable")?,
33 }
34 write!(f, ": {}\n{}", self.message, self.backtrace)
35 }
36}
37
38#[derive(Clone, Debug)]
40struct PanicLocation {
41 file: String,
42 line: u32,
43 column: u32,
44}
45
46impl From<&panic::Location<'_>> for PanicLocation {
47 fn from(loc: &panic::Location<'_>) -> Self {
48 Self {
49 file: loc.file().to_string(),
50 line: loc.line(),
51 column: loc.column(),
52 }
53 }
54}
55
56impl std::fmt::Display for PanicLocation {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 write!(f, "{}:{}:{}", self.file, self.line, self.column)
59 }
60}
61
62tokio::task_local! {
63 static BACKTRACE: RefCell<Option<PanicInfo>>;
66}
67
68pub fn set_panic_hook() {
72 panic::update_hook(move |prev, info| {
73 let backtrace = Backtrace::force_capture();
74
75 let panic_msg = if let Some(s) = info.payload_as_str() {
77 s.to_string()
78 } else {
79 "panic message was not a string".to_string()
80 };
81
82 let location = info.location().map(PanicLocation::from);
83 let loc_str = location
84 .as_ref()
85 .map_or_else(|| "unavailable".to_owned(), |l| l.to_string());
86 tracing::error!("stacktrace"=%backtrace, "panic at {loc_str}: {panic_msg}");
87
88 let _result = BACKTRACE.try_with(|entry| match entry.try_borrow_mut() {
89 Ok(mut entry_ref) => {
90 *entry_ref = Some(PanicInfo {
91 message: panic_msg,
92 location,
93 backtrace,
94 });
95 }
96 Err(borrow_mut_error) => {
97 eprintln!(
98 "failed to store backtrace to task_local: {:?}",
99 borrow_mut_error
100 );
101 }
102 });
103
104 prev(info);
106 });
107}
108
109pub(crate) async fn with_backtrace_tracking<F>(f: F) -> F::Output
112where
113 F: Future,
114{
115 BACKTRACE.scope(RefCell::new(None), f).await
116}
117
118pub(crate) fn take_panic_info() -> Result<PanicInfo, anyhow::Error> {
121 BACKTRACE
122 .try_with(|entry| {
123 entry
124 .try_borrow_mut()
125 .map_err(|e| anyhow::anyhow!("failed to borrow task_local: {:?}", e))
126 .and_then(|mut entry_ref| {
127 entry_ref
130 .take()
131 .ok_or_else(|| anyhow::anyhow!("nothing is stored in task_local"))
132 })
133 })
134 .map_err(|e| anyhow::anyhow!("failed to access task_local: {:?}", e))?
135}
136
137#[cfg(test)]
138mod tests {
139 use futures::FutureExt;
140
141 use super::*;
142
143 async fn execute_panic() {
144 let result = async {
145 panic!("boom!");
146 }
147 .catch_unwind()
148 .await;
149 assert!(result.is_err());
150 }
151
152 #[tokio::test]
153 async fn test_with_tracking() {
154 set_panic_hook();
155 with_backtrace_tracking(async {
156 execute_panic().await;
157 assert!(take_panic_info().is_ok());
159 assert!(take_panic_info().is_err());
162 })
163 .await;
164
165 assert!(take_panic_info().is_err());
168 }
169
170 #[tokio::test]
171 async fn test_without_tracking() {
172 set_panic_hook();
173 async {
174 execute_panic().await;
175 assert!(take_panic_info().is_err());
177 }
178 .await;
179 }
180
181 #[tokio::test]
182 async fn test_without_init() {
183 with_backtrace_tracking(async {
185 execute_panic().await;
186 assert!(take_panic_info().is_err());
188 })
189 .await;
190 }
191
192 #[tokio::test]
193 async fn test_nested_tasks() {
194 async fn verify_inner_panic(backtrace_captured: bool) {
195 let result = async {
196 panic!("wow!");
197 }
198 .catch_unwind()
199 .await;
200 assert!(result.is_err());
201 if backtrace_captured {
202 let info = take_panic_info().unwrap();
203 assert_eq!(info.message, "wow!");
204 assert!(info.backtrace.to_string().contains("verify_inner_panic"));
205 } else {
206 assert!(take_panic_info().is_err());
207 }
208 }
209
210 set_panic_hook();
211 with_backtrace_tracking(async {
212 execute_panic().await;
213 let result = tokio::task::spawn(async {
215 verify_inner_panic(false).await;
216 })
217 .await;
218 assert!(result.is_ok());
219
220 let result =
222 tokio::task::spawn(with_backtrace_tracking(verify_inner_panic(true))).await;
223 assert!(result.is_ok());
224
225 let info = take_panic_info().unwrap();
227 assert_eq!(info.message, "boom!");
228 assert!(info.backtrace.to_string().contains("test_nested_tasks"));
229 })
230 .await;
231 }
232}