blocking.rs (3738B)
1 use std::sync::Arc; 2 use std::time::Instant; 3 4 use tokio::runtime::Handle; 5 use tokio::sync::Semaphore; 6 7 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 8 pub(crate) enum BlockingExecutionError { 9 DeadlineElapsed, 10 Saturated, 11 TaskFailed, 12 } 13 14 #[derive(Clone)] 15 pub(crate) struct BoundedBlockingExecutor { 16 permits: Arc<Semaphore>, 17 runtime: Handle, 18 } 19 20 impl BoundedBlockingExecutor { 21 pub(crate) fn new(capacity: usize, runtime: &Handle) -> Self { 22 Self { 23 permits: Arc::new(Semaphore::new(capacity)), 24 runtime: runtime.clone(), 25 } 26 } 27 28 pub(crate) async fn execute<T, F>( 29 &self, 30 deadline: Instant, 31 operation: F, 32 ) -> Result<T, BlockingExecutionError> 33 where 34 T: Send + 'static, 35 F: FnOnce() -> T + Send + 'static, 36 { 37 if Instant::now() >= deadline { 38 return Err(BlockingExecutionError::DeadlineElapsed); 39 } 40 let permit = self 41 .permits 42 .clone() 43 .try_acquire_owned() 44 .map_err(|_| BlockingExecutionError::Saturated)?; 45 self.runtime 46 .spawn_blocking(move || { 47 let _permit = permit; 48 operation() 49 }) 50 .await 51 .map_err(|_| BlockingExecutionError::TaskFailed) 52 } 53 } 54 55 #[cfg(test)] 56 mod tests { 57 use std::sync::{Arc, Condvar, Mutex}; 58 use std::time::{Duration, Instant}; 59 60 use tokio::sync::oneshot; 61 62 use super::{BlockingExecutionError, BoundedBlockingExecutor}; 63 64 #[tokio::test(flavor = "multi_thread", worker_threads = 2)] 65 async fn executor_rejects_saturation_without_starting_excess_work() { 66 let executor = BoundedBlockingExecutor::new(1, &tokio::runtime::Handle::current()); 67 let release = Arc::new((Mutex::new(false), Condvar::new())); 68 let first_release = Arc::clone(&release); 69 let (started, started_rx) = oneshot::channel(); 70 let first_executor = executor.clone(); 71 let first = tokio::spawn(async move { 72 first_executor 73 .execute(Instant::now() + Duration::from_secs(5), move || { 74 let _ = started.send(()); 75 let (lock, ready) = &*first_release; 76 let mut released = lock.lock().expect("release lock"); 77 while !*released { 78 released = ready.wait(released).expect("release wait"); 79 } 80 7 81 }) 82 .await 83 }); 84 started_rx.await.expect("first work starts"); 85 86 let second = executor 87 .execute(Instant::now() + Duration::from_secs(5), || 9) 88 .await; 89 assert_eq!(second, Err(BlockingExecutionError::Saturated)); 90 91 let (lock, ready) = &*release; 92 *lock.lock().expect("release lock") = true; 93 ready.notify_all(); 94 assert_eq!(first.await.expect("first join"), Ok(7)); 95 } 96 97 #[tokio::test] 98 async fn executor_rejects_expired_work_before_spawn() { 99 let executor = BoundedBlockingExecutor::new(1, &tokio::runtime::Handle::current()); 100 let result = executor.execute(Instant::now(), || 1).await; 101 assert_eq!(result, Err(BlockingExecutionError::DeadlineElapsed)); 102 } 103 104 #[tokio::test] 105 async fn executor_classifies_panicked_work_without_panicking_the_actor() { 106 let executor = BoundedBlockingExecutor::new(1, &tokio::runtime::Handle::current()); 107 let result = executor 108 .execute::<(), _>(Instant::now() + Duration::from_secs(1), || { 109 panic!("test-only blocking task failure"); 110 }) 111 .await; 112 assert_eq!(result, Err(BlockingExecutionError::TaskFailed)); 113 } 114 }