|
4 | 4 | use futures::FutureExt as _; |
5 | 5 | use tokio::task::{JoinHandle, JoinSet}; |
6 | 6 |
|
| 7 | +use crate::error::{ConsensusError, ConsensusResult}; |
| 8 | + |
7 | 9 | /// Awaits the task and propagates its panic if it panicked. Task cancellation is ignored. |
8 | 10 | pub(crate) async fn join_and_propagate_panic<T>(task: JoinHandle<T>) { |
9 | 11 | if let Err(error) = task.await |
@@ -47,3 +49,58 @@ pub(crate) fn reap_finished_task(task: &mut JoinHandle<()>) -> bool { |
47 | 49 | } |
48 | 50 | true |
49 | 51 | } |
| 52 | + |
| 53 | +/// Runs the closure on the blocking pool, like tokio::task::spawn_blocking, but resolves |
| 54 | +/// to ConsensusError::Shutdown instead of a JoinError when the task is cancelled by |
| 55 | +/// runtime shutdown. Panics from the closure are resumed on the calling thread. |
| 56 | +pub(crate) async fn spawn_blocking<F, T>(f: F) -> ConsensusResult<T> |
| 57 | +where |
| 58 | + F: FnOnce() -> T + Send + 'static, |
| 59 | + T: Send + 'static, |
| 60 | +{ |
| 61 | + tokio::task::spawn_blocking(f) |
| 62 | + .await |
| 63 | + .map_err(spawn_blocking_join_error) |
| 64 | +} |
| 65 | + |
| 66 | +/// Converts a JoinError from a spawn_blocking task into a ConsensusError. |
| 67 | +/// Panics from the blocking task are resumed on the calling thread. Otherwise the task |
| 68 | +/// was cancelled, which only happens when the runtime is shutting down. |
| 69 | +fn spawn_blocking_join_error(e: tokio::task::JoinError) -> ConsensusError { |
| 70 | + if e.is_panic() { |
| 71 | + std::panic::resume_unwind(e.into_panic()); |
| 72 | + } |
| 73 | + ConsensusError::Shutdown |
| 74 | +} |
| 75 | + |
| 76 | +#[cfg(test)] |
| 77 | +mod test { |
| 78 | + use super::*; |
| 79 | + |
| 80 | + /// Reproduces the production scenario: spawn_blocking racing with runtime shutdown |
| 81 | + /// gets its task cancelled instead of run, so its JoinHandle resolves to |
| 82 | + /// JoinError::Cancelled. |
| 83 | + #[tokio::test] |
| 84 | + async fn test_spawn_blocking_join_error_on_runtime_shutdown() { |
| 85 | + let runtime = tokio::runtime::Builder::new_multi_thread() |
| 86 | + .worker_threads(1) |
| 87 | + .build() |
| 88 | + .unwrap(); |
| 89 | + let handle = runtime.handle().clone(); |
| 90 | + runtime.shutdown_background(); |
| 91 | + |
| 92 | + let cancelled = handle.spawn_blocking(|| ()); |
| 93 | + let join_error = cancelled.await.unwrap_err(); |
| 94 | + assert!(join_error.is_cancelled()); |
| 95 | + assert!(matches!( |
| 96 | + spawn_blocking_join_error(join_error), |
| 97 | + ConsensusError::Shutdown |
| 98 | + )); |
| 99 | + } |
| 100 | + |
| 101 | + #[tokio::test] |
| 102 | + #[should_panic(expected = "boom")] |
| 103 | + async fn test_spawn_blocking_resumes_panic() { |
| 104 | + let _ = spawn_blocking(|| panic!("boom")).await; |
| 105 | + } |
| 106 | +} |
0 commit comments