fix(core): wrap fire-and-forget spawn_blocking calls with panic dump … (#810)
* fix(core): wrap fire-and-forget spawn_blocking calls with panic dump protection * test(utils): cover supervised blocking panic handling --------- Co-authored-by: Hunter Bown <hmbown@gmail.com>
This commit is contained in:
@@ -1013,7 +1013,7 @@ impl Engine {
|
||||
if self.config.snapshots_enabled {
|
||||
let post_workspace = self.session.workspace.clone();
|
||||
let post_seq = self.turn_counter;
|
||||
tokio::task::spawn_blocking(move || {
|
||||
crate::utils::spawn_blocking_supervised("post-turn-snapshot", move || {
|
||||
post_turn_snapshot(&post_workspace, post_seq);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ impl FileTreeState {
|
||||
let loading_cell = Arc::new(Mutex::new(None));
|
||||
let cell = loading_cell.clone();
|
||||
let ws = workspace.to_path_buf();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
crate::utils::spawn_blocking_supervised("file-tree-build", move || {
|
||||
let entries = build_file_tree_inner(&ws, &HashSet::new(), None);
|
||||
if let Ok(mut guard) = cell.lock() {
|
||||
*guard = Some(entries);
|
||||
|
||||
@@ -274,6 +274,40 @@ fn write_panic_dump_to(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fire-and-forget `spawn_blocking` with panic dump protection.
|
||||
///
|
||||
/// In contrast to `spawn_supervised` (which wraps `tokio::spawn` for async
|
||||
/// tasks), this helper wraps `tokio::task::spawn_blocking`. Use it when a
|
||||
/// CPU-bound or blocking-I/O task must run off the async runtime and its
|
||||
/// completion is *not* awaited — for example a post-turn disk snapshot or a
|
||||
/// file-tree build polled later via a shared data structure. If the closure
|
||||
/// panics, a crash dump is written to `~/.deepseek/crashes/` and the panic
|
||||
/// is logged at ERROR level rather than being silently swallowed.
|
||||
#[track_caller]
|
||||
pub fn spawn_blocking_supervised<F>(name: &'static str, f: F) -> tokio::task::JoinHandle<()>
|
||||
where
|
||||
F: FnOnce() + Send + 'static,
|
||||
{
|
||||
let location = std::panic::Location::caller();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
|
||||
if let Err(panic_info) = result {
|
||||
let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
|
||||
s.to_string()
|
||||
} else if let Some(s) = panic_info.downcast_ref::<String>() {
|
||||
s.clone()
|
||||
} else {
|
||||
"unknown panic".to_string()
|
||||
};
|
||||
tracing::error!(
|
||||
target: "panic",
|
||||
"Blocking task '{name}' panicked at {location}: {msg}",
|
||||
);
|
||||
let _ = write_panic_dump(name, location, &msg);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn ensure_dir(path: &Path) -> Result<()> {
|
||||
fs::create_dir_all(path)
|
||||
@@ -579,6 +613,27 @@ mod spawn_supervised_tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn panicking_blocking_task_does_not_propagate_to_parent() {
|
||||
let parent_alive = Arc::new(AtomicBool::new(false));
|
||||
let parent_alive_clone = parent_alive.clone();
|
||||
|
||||
let handle = spawn_blocking_supervised("blocking-panic-test-fixture", move || {
|
||||
parent_alive_clone.store(true, Ordering::SeqCst);
|
||||
panic!("deliberate panic for spawn_blocking catch-unwind test");
|
||||
});
|
||||
|
||||
let result = handle.await;
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"spawn_blocking_supervised must convert panic to a normal completion"
|
||||
);
|
||||
assert!(
|
||||
parent_alive.load(Ordering::SeqCst),
|
||||
"fixture blocking task must have run before panicking"
|
||||
);
|
||||
}
|
||||
|
||||
/// `write_panic_dump_to` writes a properly-formatted crash log into
|
||||
/// the supplied directory. Tested separately from `spawn_supervised`
|
||||
/// because env-mutation redirection of `dirs::home_dir()` doesn't
|
||||
|
||||
Reference in New Issue
Block a user