Files
ferrix/crates/server/src/panic.rs
T
Rayhaan JaufeerallyandClaude Fable 5.1 a772322d9e Unwind panics safely: say goodbye to the peer, keep the other sessions
A panic is a bug, and it should cost as little as it can. The server
already closed every session with a Cease and exited when a RIB or a
background task was lost. This fills in the rest:

- A panic inside a session is caught. The peer is sent Cease /
  Administrative Reset, the session's routes are removed in the usual
  order, and the dialer reconnects. Before, the peer was left with a TCP
  connection closing under it and no reason.
- The tables every session shares (the session registry and the collision
  table) no longer refuse to be locked after a panic. Lock poisoning would
  have made each remaining session panic in turn the next time it touched
  one, so one bug in one session took all of them down.
- The server's watcher reports a task that panicked with the panic's
  message, and is now tested for both a panicking task and a stopped RIB.
- The daemon sends panics to its log, with their location, instead of
  leaving them to the default report on stderr.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-01 23:24:00 +02:00

61 lines
2.1 KiB
Rust

//! Containing panics.
//!
//! A panic is a bug. It unwinds the task it happens in, and what that task
//! was responsible for still has to be finished properly: a session tells
//! its peer it is closing and gives up its routes; the server, if it lost
//! something it cannot do without, closes every session and exits.
use std::any::Any;
use std::panic::AssertUnwindSafe;
use futures::FutureExt;
/// Runs `future` to completion. If it panics, the panic is caught and its
/// message returned, so that the caller can clean up after it.
///
/// The caller must not go on using what the future was working on as if
/// nothing had happened: a panic can leave it half changed. Closing it down
/// is what this is for.
pub(crate) async fn contained<F: Future>(future: F) -> Result<F::Output, String> {
AssertUnwindSafe(future)
.catch_unwind()
.await
.map_err(|payload| message(payload.as_ref()))
}
/// The text of a panic, from what `catch_unwind` or a `JoinError` carries.
pub(crate) fn message(payload: &(dyn Any + Send)) -> String {
if let Some(text) = payload.downcast_ref::<&'static str>() {
(*text).to_string()
} else if let Some(text) = payload.downcast_ref::<String>() {
text.clone()
} else {
"a panic that carried no message".to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn a_panic_is_caught_with_its_message() {
assert_eq!(contained(async { 7 }).await, Ok(7));
let literal: Result<(), _> = contained(async { panic!("the table is gone") }).await;
assert_eq!(literal, Err("the table is gone".to_string()));
let formatted: Result<(), _> =
contained(async { panic!("index {} out of range", 9) }).await;
assert_eq!(formatted, Err("index 9 out of range".to_string()));
// After a point where the future had been suspended, too.
let later: Result<(), _> = contained(async {
tokio::task::yield_now().await;
panic!("later")
})
.await;
assert_eq!(later, Err("later".to_string()));
}
}