1use std::fmt;
4
5static WORKER_SLOTS: tokio::sync::Semaphore = tokio::sync::Semaphore::const_new(512);
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum BlockingWorkError {
12 WorkerPanicked,
13 WorkerCancelled,
14 ResultChannelClosed,
15 RuntimeUnavailable,
16 AllocationFailed,
17 ThreadSpawnFailed,
18}
19
20impl fmt::Display for BlockingWorkError {
21 fn fmt(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result {
22 out.write_str(match self {
23 Self::WorkerPanicked => "blocking worker panicked",
24 Self::WorkerCancelled => "blocking worker was cancelled before returning",
25 Self::ResultChannelClosed => "blocking worker result channel closed before returning",
26 Self::RuntimeUnavailable => "blocking worker requires a Tokio runtime",
27 Self::AllocationFailed => "could not reserve blocking worker ownership records",
28 Self::ThreadSpawnFailed => "could not create blocking worker thread",
29 })
30 }
31}
32
33impl std::error::Error for BlockingWorkError {}
34
35#[derive(Debug)]
37pub struct BlockingWorkDrainError {
38 errors: Vec<BlockingWorkError>,
39}
40
41impl BlockingWorkDrainError {
42 pub fn errors(&self) -> &[BlockingWorkError] {
43 &self.errors
44 }
45}
46
47impl fmt::Display for BlockingWorkDrainError {
48 fn fmt(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result {
49 for (index, error) in self.errors.iter().enumerate() {
50 if index > 0 {
51 out.write_str("; ")?;
52 }
53 error.fmt(out)?;
54 }
55 Ok(())
56 }
57}
58
59impl std::error::Error for BlockingWorkDrainError {}
60
61#[derive(Default)]
64pub struct BlockingWork {
65 pending: Vec<Worker>,
66 drain_failures: Vec<BlockingWorkError>,
67}
68
69impl BlockingWork {
70 pub async fn spawn<T: Send + 'static>(
75 &mut self,
76 work: impl FnOnce() -> T + Send + 'static,
77 ) -> Result<T, BlockingWorkError> {
78 self.spawn_with_slots(work, &WORKER_SLOTS).await
79 }
80
81 async fn spawn_with_slots<T: Send + 'static>(
82 &mut self,
83 work: impl FnOnce() -> T + Send + 'static,
84 slots: &'static tokio::sync::Semaphore,
85 ) -> Result<T, BlockingWorkError> {
86 tokio::runtime::Handle::try_current().map_err(|_| BlockingWorkError::RuntimeUnavailable)?;
87 let permit = slots
88 .acquire()
89 .await
90 .map_err(|_| BlockingWorkError::WorkerCancelled)?;
91 self.reserve_worker()?;
92 let (sender, receiver) = tokio::sync::oneshot::channel();
93 let worker = Worker::start(
94 move || {
95 let _ = sender.send(work());
97 },
98 Some(permit),
99 )?;
100 self.pending.push(worker);
101 self.join_last().await?;
102 receive_result(receiver).await
103 }
104
105 pub async fn finish(&mut self) -> Result<(), BlockingWorkDrainError> {
108 while !self.pending.is_empty() {
109 if let Err(error) = self.join_last().await {
110 self.drain_failures.push(error);
112 }
113 }
114 if self.drain_failures.is_empty() {
115 return Ok(());
116 }
117 Err(BlockingWorkDrainError {
118 errors: std::mem::take(&mut self.drain_failures),
119 })
120 }
121
122 fn reserve_worker(&mut self) -> Result<(), BlockingWorkError> {
123 let outstanding = self
124 .pending
125 .len()
126 .checked_add(1)
127 .ok_or(BlockingWorkError::AllocationFailed)?;
128 self.pending
129 .try_reserve(1)
130 .map_err(|_| BlockingWorkError::AllocationFailed)?;
131 self.drain_failures
132 .try_reserve(outstanding)
133 .map_err(|_| BlockingWorkError::AllocationFailed)?;
134 Ok(())
135 }
136
137 async fn join_last(&mut self) -> Result<(), BlockingWorkError> {
138 let worker = self
139 .pending
140 .last_mut()
141 .ok_or(BlockingWorkError::ResultChannelClosed)?;
142 worker.wait().await;
143 let worker = self
144 .pending
145 .pop()
146 .ok_or(BlockingWorkError::ResultChannelClosed)?;
147 worker.join()
148 }
149}
150
151struct Worker {
152 thread: std::thread::JoinHandle<Result<(), BlockingWorkError>>,
153 completed: Option<tokio::sync::oneshot::Receiver<()>>,
154}
155
156impl Worker {
157 fn start(
158 work: impl FnOnce() + Send + 'static,
159 permit: Option<tokio::sync::SemaphorePermit<'static>>,
160 ) -> Result<Self, BlockingWorkError> {
161 Self::start_with(std::thread::Builder::new(), work, permit)
162 }
163
164 fn start_with(
165 builder: std::thread::Builder,
166 work: impl FnOnce() + Send + 'static,
167 permit: Option<tokio::sync::SemaphorePermit<'static>>,
168 ) -> Result<Self, BlockingWorkError> {
169 let runtime = tokio::runtime::Handle::try_current()
170 .map_err(|_| BlockingWorkError::RuntimeUnavailable)?;
171 let (sender, completed) = tokio::sync::oneshot::channel();
172 let thread = builder
173 .spawn(move || {
174 let _permit = permit;
175 let _completion = Completion(Some(sender));
176 let _runtime = runtime.enter();
177 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(work)) {
180 Ok(()) => Ok(()),
181 Err(payload) => {
182 dispose_panic_payload(payload);
183 Err(BlockingWorkError::WorkerPanicked)
184 }
185 }
186 })
187 .map_err(|_| BlockingWorkError::ThreadSpawnFailed)?;
188 Ok(Self {
189 thread,
190 completed: Some(completed),
191 })
192 }
193
194 async fn wait(&mut self) {
195 if let Some(completed) = self.completed.as_mut() {
196 let _ = completed.await;
197 self.completed = None;
198 }
199 while !self.thread.is_finished() {
202 tokio::task::yield_now().await;
203 }
204 }
205
206 fn join(self) -> Result<(), BlockingWorkError> {
207 match self.thread.join() {
208 Ok(result) => result,
209 Err(payload) => {
210 dispose_panic_payload(payload);
211 Err(BlockingWorkError::WorkerPanicked)
212 }
213 }
214 }
215}
216
217struct Completion(Option<tokio::sync::oneshot::Sender<()>>);
218
219impl Drop for Completion {
220 fn drop(&mut self) {
221 if let Some(sender) = self.0.take() {
222 let _ = sender.send(());
223 }
224 }
225}
226
227fn dispose_panic_payload(mut payload: Box<dyn std::any::Any + Send>) {
228 loop {
232 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| drop(payload))) {
233 Ok(()) => return,
234 Err(replacement) => payload = replacement,
235 }
236 }
237}
238
239async fn receive_result<T>(
240 receiver: tokio::sync::oneshot::Receiver<T>,
241) -> Result<T, BlockingWorkError> {
242 receiver
243 .await
244 .map_err(|_| BlockingWorkError::ResultChannelClosed)
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use std::time::Duration;
251
252 #[tokio::test]
253 async fn worker_panics_return_errors_and_allow_follow_up() {
254 let mut workers = BlockingWork::default();
255 assert_eq!(
256 workers.spawn(|| panic!("injected worker panic")).await,
257 Err(BlockingWorkError::WorkerPanicked)
258 );
259 assert_eq!(
260 workers.spawn(|| std::panic::panic_any(42_u32)).await,
261 Err(BlockingWorkError::WorkerPanicked)
262 );
263 assert!(workers.finish().await.is_ok());
264 assert_eq!(workers.spawn(|| 42).await, Ok(42));
265 assert_eq!(
266 workers.spawn(|| Err::<(), _>("operation failed")).await,
267 Ok(Err("operation failed"))
268 );
269 }
270
271 #[test]
272 fn spawning_without_runtime_returns_error() {
273 let mut workers = BlockingWork::default();
274 let mut future = Box::pin(workers.spawn(|| 42));
275 let mut context = std::task::Context::from_waker(std::task::Waker::noop());
276 assert!(matches!(
277 std::future::Future::poll(future.as_mut(), &mut context),
278 std::task::Poll::Ready(Err(BlockingWorkError::RuntimeUnavailable))
279 ));
280 drop(future);
281 assert!(workers.pending.is_empty());
282 }
283
284 #[tokio::test]
285 async fn closed_result_channel_returns_error() {
286 let (sender, receiver) = tokio::sync::oneshot::channel::<()>();
287 drop(sender);
288 assert_eq!(
289 receive_result(receiver).await,
290 Err(BlockingWorkError::ResultChannelClosed)
291 );
292 }
293
294 #[tokio::test]
295 async fn thread_admission_failure_returns_error_and_allows_follow_up() {
296 let result = Worker::start_with(
297 std::thread::Builder::new().stack_size(usize::MAX),
298 || {},
299 None,
300 );
301 assert!(matches!(result, Err(BlockingWorkError::ThreadSpawnFailed)));
302 let mut workers = BlockingWork::default();
303 assert_eq!(workers.spawn(|| 42).await, Ok(42));
304 workers.finish().await.unwrap();
305 }
306
307 #[tokio::test]
308 async fn panicking_payload_destructors_are_disposed_without_interrupting_drain() {
309 struct Payload(std::sync::Arc<std::sync::atomic::AtomicUsize>, u8);
310 impl Drop for Payload {
311 fn drop(&mut self) {
312 self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
313 if self.1 > 0 {
314 std::panic::panic_any(Payload(self.0.clone(), self.1 - 1));
315 }
316 }
317 }
318 let drops = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
319 let mark = drops.clone();
320 let mut workers = BlockingWork::default();
321 assert_eq!(
322 workers
323 .spawn(move || std::panic::panic_any(Payload(mark, 3)))
324 .await,
325 Err(BlockingWorkError::WorkerPanicked)
326 );
327 assert_eq!(drops.load(std::sync::atomic::Ordering::SeqCst), 4);
328 assert_eq!(workers.spawn(|| 42).await, Ok(42));
329 workers.finish().await.unwrap();
330 }
331
332 #[tokio::test]
333 async fn native_workers_preserve_runtime_context() {
334 let mut workers = BlockingWork::default();
335 assert_eq!(
336 workers
337 .spawn(|| { tokio::runtime::Handle::current().block_on(async { 42 }) })
338 .await,
339 Ok(42)
340 );
341 }
342
343 #[tokio::test]
344 async fn admission_waits_for_abandoned_worker_cleanup_and_can_be_cancelled() {
345 static SLOTS: tokio::sync::Semaphore = tokio::sync::Semaphore::const_new(1);
346 let mut first = BlockingWork::default();
347 let mut second = BlockingWork::default();
348 let (started, ready) = tokio::sync::oneshot::channel();
349 let (release, gate) = std::sync::mpsc::channel();
350 let mut waiter = Box::pin(first.spawn_with_slots(
351 move || {
352 started.send(()).unwrap();
353 gate.recv_timeout(Duration::from_secs(5)).unwrap();
354 },
355 &SLOTS,
356 ));
357 tokio::select! {
358 result = &mut waiter => panic!("worker ended early: {result:?}"),
359 result = ready => result.unwrap(),
360 }
361 drop(waiter);
362 let called = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
363 let mark = called.clone();
364 assert!(
365 tokio::time::timeout(
366 Duration::from_millis(20),
367 second.spawn_with_slots(
368 move || mark.store(true, std::sync::atomic::Ordering::SeqCst),
369 &SLOTS
370 )
371 )
372 .await
373 .is_err()
374 );
375 assert!(second.pending.is_empty());
376 assert!(!called.load(std::sync::atomic::Ordering::SeqCst));
377 release.send(()).unwrap();
378 first.finish().await.unwrap();
379 assert_eq!(second.spawn_with_slots(|| 42, &SLOTS).await, Ok(42));
380 }
381
382 #[tokio::test]
383 async fn completed_abandoned_worker_releases_admission_before_join() {
384 static SLOTS: tokio::sync::Semaphore = tokio::sync::Semaphore::const_new(1);
385 let mut workers = BlockingWork::default();
386 let (started, ready) = tokio::sync::oneshot::channel();
387 let (release, gate) = std::sync::mpsc::channel();
388 let mut waiter = Box::pin(workers.spawn_with_slots(
389 move || {
390 started.send(()).unwrap();
391 gate.recv_timeout(Duration::from_secs(5)).unwrap();
392 panic!("abandoned failure");
393 },
394 &SLOTS,
395 ));
396 tokio::select! {
397 result = &mut waiter => panic!("worker ended early: {result:?}"),
398 result = ready => result.unwrap(),
399 }
400 drop(waiter);
401 release.send(()).unwrap();
402 assert_eq!(
403 tokio::time::timeout(
404 Duration::from_secs(5),
405 workers.spawn_with_slots(|| 42, &SLOTS)
406 )
407 .await
408 .unwrap(),
409 Ok(42)
410 );
411 assert_eq!(workers.pending.len(), 1);
412 assert_eq!(
413 workers.finish().await.unwrap_err().errors(),
414 &[BlockingWorkError::WorkerPanicked]
415 );
416 }
417
418 #[tokio::test]
419 async fn payload_cleanup_stays_owned_and_does_not_block_executor() {
420 struct Payload {
421 started: Option<tokio::sync::oneshot::Sender<()>>,
422 gate: std::sync::mpsc::Receiver<()>,
423 }
424 impl Drop for Payload {
425 fn drop(&mut self) {
426 self.started.take().unwrap().send(()).unwrap();
427 self.gate.recv_timeout(Duration::from_secs(5)).unwrap();
428 panic!("payload destructor failure");
429 }
430 }
431 let mut workers = BlockingWork::default();
432 let (started, ready) = tokio::sync::oneshot::channel();
433 let (release, gate) = std::sync::mpsc::channel();
434 let mut waiter = Box::pin(workers.spawn(move || {
435 std::panic::panic_any(Payload {
436 started: Some(started),
437 gate,
438 });
439 }));
440 tokio::select! {
441 result = &mut waiter => panic!("worker ended early: {result:?}"),
442 result = ready => result.unwrap(),
443 }
444 drop(waiter);
445 assert!(
446 tokio::time::timeout(Duration::from_millis(20), workers.finish())
447 .await
448 .is_err()
449 );
450 assert_eq!(workers.pending.len(), 1);
451 release.send(()).unwrap();
452 let failures = workers.finish().await.unwrap_err();
453 assert_eq!(failures.errors(), &[BlockingWorkError::WorkerPanicked]);
454 assert_eq!(workers.spawn(|| 42).await, Ok(42));
455 }
456
457 #[tokio::test]
458 async fn cancelled_waiter_retains_actual_worker_and_result_cleanup() {
459 struct Output(std::sync::Arc<std::sync::atomic::AtomicBool>);
460 impl Drop for Output {
461 fn drop(&mut self) {
462 self.0.store(true, std::sync::atomic::Ordering::SeqCst);
463 }
464 }
465 let mut workers = BlockingWork::default();
466 let (started, ready) = tokio::sync::oneshot::channel();
467 let (release, gate) = std::sync::mpsc::channel();
468 let dropped = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
469 let mark = dropped.clone();
470 let mut waiter = Box::pin(workers.spawn(move || {
471 started.send(()).unwrap();
472 gate.recv_timeout(Duration::from_secs(5)).unwrap();
473 Output(mark)
474 }));
475 tokio::select! {
476 result = &mut waiter => panic!("worker returned early: {}", result.is_ok()),
477 result = ready => result.unwrap(),
478 }
479 drop(waiter);
480 assert_eq!(workers.spawn(|| 42).await, Ok(42));
481 assert_eq!(workers.pending.len(), 1);
482 let mut finish = Box::pin(workers.finish());
483 assert!(
484 tokio::time::timeout(Duration::from_millis(20), &mut finish)
485 .await
486 .is_err()
487 );
488 assert!(!dropped.load(std::sync::atomic::Ordering::SeqCst));
489 release.send(()).unwrap();
490 finish.await.unwrap();
491 assert!(dropped.load(std::sync::atomic::Ordering::SeqCst));
492 }
493
494 #[tokio::test]
495 async fn cancelled_drain_retains_handles_and_every_observed_failure() {
496 let mut workers = BlockingWork::default();
497 let (release, gate) = std::sync::mpsc::channel();
498 workers.reserve_worker().unwrap();
499 workers.pending.push(
500 Worker::start(
501 move || {
502 gate.recv_timeout(Duration::from_secs(5)).unwrap();
503 panic!("late failure");
504 },
505 None,
506 )
507 .unwrap(),
508 );
509 for _ in 0..2 {
510 workers.reserve_worker().unwrap();
511 let task = Worker::start(|| panic!("early failure"), None).unwrap();
512 while !task.thread.is_finished() {
513 tokio::task::yield_now().await;
514 }
515 workers.pending.push(task);
516 }
517 assert!(
518 tokio::time::timeout(Duration::from_millis(20), workers.finish())
519 .await
520 .is_err()
521 );
522 assert_eq!(workers.pending.len(), 1);
523 assert_eq!(
524 workers.drain_failures,
525 vec![BlockingWorkError::WorkerPanicked; 2]
526 );
527 release.send(()).unwrap();
528 let errors = workers.finish().await.unwrap_err();
529 assert_eq!(errors.errors(), &[BlockingWorkError::WorkerPanicked; 3]);
530 assert!(workers.pending.is_empty());
531 assert!(workers.finish().await.is_ok());
532 }
533}