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>
61 lines
2.1 KiB
Rust
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()));
|
|
}
|
|
}
|