Skip to content

Commit ac0550c

Browse files
committed
feat(utils/error): make catching panics work correctly in a multi-threaded setting
1 parent 7560d3d commit ac0550c

File tree

4 files changed

+105
-66
lines changed

4 files changed

+105
-66
lines changed

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,6 @@ version = "1.14"
517517

518518
[workspace.dependencies.tokio]
519519
version = "1"
520-
default-features = false
521520

522521
[workspace.dependencies.tempfile]
523522
version = "3.15"

utilities/src/errors.rs

Lines changed: 92 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,16 @@
1414
// limitations under the License.
1515

1616
use colored::Colorize;
17-
use std::borrow::Borrow;
17+
18+
use std::{any::Any, backtrace::Backtrace, borrow::Borrow, cell::Cell, panic};
19+
20+
thread_local! {
21+
/// The message backtrace of the last panic on this thread (if any).
22+
///
23+
/// We store this information here instead of directly processing it in a panic hook, because panic hooks are global whereas this can be processed on a per-thread basis.
24+
/// For example, one thread may execute a program where panics should *not* cause the entire process to terminate, while in another thread there is a panic due to a bug.
25+
static PANIC_INFO: Cell<Option<(String, Backtrace)>> = const { Cell::new(None) };
26+
}
1827

1928
/// Generates an `io::Error` from the given string.
2029
#[inline]
@@ -80,7 +89,17 @@ pub trait PrettyUnwrap {
8089
fn pretty_expect<S: ToString>(self, context: S) -> Self::Inner;
8190
}
8291

83-
/// Helper for `PrettyUnwrap`, which creates a panic with the `anyhow::Error` nicely formatted and also logs the panic.
92+
/// Set the global panic hook for process. Should be called exactly once.
93+
pub fn set_panic_hook() {
94+
std::panic::set_hook(Box::new(|err| {
95+
let msg = err.to_string();
96+
let trace = Backtrace::force_capture();
97+
PANIC_INFO.with(move |info| info.set(Some((msg, trace))));
98+
}));
99+
}
100+
101+
/// Helper for `PrettyUnwrap`:
102+
/// Creates a panic with the `anyhow::Error` nicely formatted.
84103
#[track_caller]
85104
#[inline]
86105
fn pretty_panic(error: &anyhow::Error) -> ! {
@@ -97,6 +116,7 @@ impl<T> PrettyUnwrap for anyhow::Result<T> {
97116
type Inner = T;
98117

99118
#[track_caller]
119+
#[inline]
100120
fn pretty_unwrap(self) -> Self::Inner {
101121
match self {
102122
Ok(result) => result,
@@ -117,9 +137,59 @@ impl<T> PrettyUnwrap for anyhow::Result<T> {
117137
}
118138
}
119139

140+
/// `try_vm_runtime` executes the given closure in an environment which will safely halt
141+
/// without producing logs that look like unexpected behavior.
142+
/// In debug mode, it prints to stderr using the format: "VM safely halted at {location}: {halt message}".
143+
///
144+
/// Note: For this to work as expected, panics must be set to `unwind` during compilation (default), and the closure cannot invoke any async code that may potentially execute in a different OS thread.
145+
#[track_caller]
146+
#[inline]
147+
pub fn try_vm_runtime<R, F: FnMut() -> R>(f: F) -> Result<R, Box<dyn Any + Send>> {
148+
// Perform the operation that may panic.
149+
let result = std::panic::catch_unwind(panic::AssertUnwindSafe(f));
150+
151+
if result.is_err() {
152+
// Get the stored panic and backtrace from the thread-local variable.
153+
let (msg, _) = PANIC_INFO.with(|info| info.take()).expect("No panic information stored?");
154+
155+
#[cfg(debug_assertions)]
156+
{
157+
// Remove all words up to "panicked".
158+
// And prepend with "VM Safely halted"
159+
let msg = msg
160+
.to_string()
161+
.split_ascii_whitespace()
162+
.skip_while(|&word| word != "panicked")
163+
.collect::<Vec<&str>>()
164+
.join(" ")
165+
.replacen("panicked", "VM safely halted", 1);
166+
167+
eprintln!("{msg}");
168+
}
169+
#[cfg(not(debug_assertions))]
170+
{
171+
// Discard message
172+
let _ = msg;
173+
}
174+
}
175+
176+
// Return the result, allowing regular error-handling.
177+
result
178+
}
179+
180+
/// `catch_unwind` calls the given closure `f` and, if `f` panics, returns the panic message and backtrace.
181+
#[inline]
182+
pub fn catch_unwind<R, F: FnMut() -> R>(f: F) -> Result<R, (String, Backtrace)> {
183+
// Perform the operation that may panic.
184+
std::panic::catch_unwind(panic::AssertUnwindSafe(f)).map_err(|_| {
185+
// Get the stored panic and backtrace from the thread-local variable.
186+
PANIC_INFO.with(|info| info.take()).expect("No panic information stored?")
187+
})
188+
}
189+
120190
#[cfg(test)]
121191
mod tests {
122-
use super::{PrettyUnwrap, flatten_error, pretty_panic};
192+
use super::{PrettyUnwrap, catch_unwind, flatten_error, pretty_panic, set_panic_hook, try_vm_runtime};
123193

124194
use anyhow::{Context, Result, anyhow, bail};
125195
use colored::Colorize;
@@ -177,14 +247,31 @@ mod tests {
177247
assert_eq!(*result.downcast::<String>().expect("Error was not a string"), expected);
178248
}
179249

250+
// Ensure catch_unwind stores the panic message as expected.
251+
#[test]
252+
fn test_catch_unwind() {
253+
set_panic_hook();
254+
let result = catch_unwind(move || {
255+
panic!("This is my message");
256+
});
257+
// Remove hook so test asserts work normally again.
258+
let _ = std::panic::take_hook();
259+
260+
let (msg, bt) = result.expect_err("No panic caught");
261+
assert!(msg.ends_with("This is my message"));
262+
263+
// This function should be in the panics backtrace
264+
assert!(bt.to_string().contains("test_catch_unwind"));
265+
}
266+
180267
/// Ensure catch_unwind does not break `try_vm_runtime`.
181268
#[test]
182269
fn test_nested_with_try_vm_runtime() {
183-
use crate::try_vm_runtime;
270+
set_panic_hook();
184271

185272
let result = std::panic::catch_unwind(|| {
186273
// try_vm_runtime uses catch_unwind internally
187-
let vm_result = try_vm_runtime!(|| {
274+
let vm_result = try_vm_runtime(|| {
188275
panic!("VM operation failed!");
189276
});
190277

utilities/src/lib.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,6 @@ pub use bytes::*;
3434
pub mod defer;
3535
pub use defer::*;
3636

37-
mod vm_error;
38-
pub use vm_error::*;
39-
4037
pub mod iterator;
4138
pub use iterator::*;
4239

@@ -66,3 +63,16 @@ pub use task::*;
6663

6764
/// Use old name for backward-compatibility.
6865
pub use errors::io_error as error;
66+
67+
/// This macro provides a VM runtime environment which will safely halt
68+
/// without producing logs that look like unexpected behavior.
69+
/// In debug mode, it prints to stderr using the format: "VM safely halted at {location}: {halt message}".
70+
///
71+
/// It is more efficient to set the panic hook once and directly use `errors::try_vm_runtime`.
72+
#[macro_export]
73+
macro_rules! try_vm_runtime {
74+
($e:expr) => {{
75+
$crate::errors::set_panic_hook();
76+
$crate::errors::try_vm_runtime($e)
77+
}};
78+
}

utilities/src/vm_error.rs

Lines changed: 0 additions & 57 deletions
This file was deleted.

0 commit comments

Comments
 (0)