1use crate::metrics::WorkerPoolMetrics;
4use std::{
5 any::Any,
6 cell::RefCell,
7 future::Future,
8 ops::Deref,
9 panic::{catch_unwind, AssertUnwindSafe},
10 pin::Pin,
11 sync::{
12 atomic::{AtomicUsize, Ordering},
13 Arc, OnceLock, Weak,
14 },
15 task::{ready, Context, Poll},
16 thread,
17 time::Instant,
18};
19use tokio::sync::{oneshot, AcquireError, OwnedSemaphorePermit, Semaphore};
20
21#[derive(Clone, Debug)]
30pub struct BlockingTaskGuard(Arc<Semaphore>);
31
32impl BlockingTaskGuard {
33 pub fn new(max_blocking_tasks: usize) -> Self {
36 Self(Arc::new(Semaphore::new(max_blocking_tasks)))
37 }
38
39 pub async fn acquire_owned(self) -> Result<OwnedSemaphorePermit, AcquireError> {
41 self.0.acquire_owned().await
42 }
43
44 pub async fn acquire_many_owned(self, n: u32) -> Result<OwnedSemaphorePermit, AcquireError> {
46 self.0.acquire_many_owned(n).await
47 }
48}
49
50#[derive(Clone, Debug)]
63pub struct BlockingTaskPool {
64 pool: Arc<rayon::ThreadPool>,
65}
66
67impl BlockingTaskPool {
68 pub fn new(pool: rayon::ThreadPool) -> Self {
70 Self { pool: Arc::new(pool) }
71 }
72
73 pub fn builder() -> rayon::ThreadPoolBuilder {
75 rayon::ThreadPoolBuilder::new()
76 }
77
78 pub fn build() -> Result<Self, rayon::ThreadPoolBuildError> {
84 Self::builder().build().map(Self::new)
85 }
86
87 pub fn spawn<F, R>(&self, func: F) -> BlockingTaskHandle<R>
95 where
96 F: FnOnce() -> R + Send + 'static,
97 R: Send + 'static,
98 {
99 let (tx, rx) = oneshot::channel();
100
101 self.pool.spawn(move || {
102 let _result = tx.send(catch_unwind(AssertUnwindSafe(func)));
103 });
104
105 BlockingTaskHandle { rx }
106 }
107
108 pub fn spawn_fifo<F, R>(&self, func: F) -> BlockingTaskHandle<R>
116 where
117 F: FnOnce() -> R + Send + 'static,
118 R: Send + 'static,
119 {
120 let (tx, rx) = oneshot::channel();
121
122 self.pool.spawn_fifo(move || {
123 let _result = tx.send(catch_unwind(AssertUnwindSafe(func)));
124 });
125
126 BlockingTaskHandle { rx }
127 }
128}
129
130#[derive(Debug)]
136#[must_use = "futures do nothing unless you `.await` or poll them"]
137#[pin_project::pin_project]
138pub struct BlockingTaskHandle<T> {
139 #[pin]
140 pub(crate) rx: oneshot::Receiver<thread::Result<T>>,
141}
142
143impl<T> Future for BlockingTaskHandle<T> {
144 type Output = thread::Result<T>;
145
146 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
147 match ready!(self.project().rx.poll(cx)) {
148 Ok(res) => Poll::Ready(res),
149 Err(_) => Poll::Ready(Err(Box::<TokioBlockingTaskError>::default())),
150 }
151 }
152}
153
154#[derive(Debug, Default, thiserror::Error)]
158#[error("tokio channel dropped while awaiting result")]
159#[non_exhaustive]
160pub struct TokioBlockingTaskError;
161
162thread_local! {
163 static WORKER: RefCell<Worker> = const { RefCell::new(Worker::new()) };
164}
165
166#[derive(Debug)]
181pub struct WorkerPool {
182 pool: OnceLock<rayon::ThreadPool>,
183 metrics: OnceLock<WorkerPoolMetrics>,
184 num_threads: usize,
185 thread_name_prefix: &'static str,
186}
187
188impl WorkerPool {
189 pub const fn new(num_threads: usize, thread_name_prefix: &'static str) -> Self {
194 Self { pool: OnceLock::new(), metrics: OnceLock::new(), num_threads, thread_name_prefix }
195 }
196
197 fn pool(&self) -> &rayon::ThreadPool {
199 self.pool.get_or_init(|| {
200 let prefix = self.thread_name_prefix;
201 build_pool_with_panic_handler(
202 rayon::ThreadPoolBuilder::new()
203 .num_threads(self.num_threads)
204 .thread_name(move |i| format!("{prefix}-{i:02}")),
205 )
206 .unwrap_or_else(|err| panic!("failed to build {prefix} worker pool: {err}"))
207 })
208 }
209
210 fn metrics(&self) -> &WorkerPoolMetrics {
212 self.metrics.get_or_init(|| WorkerPoolMetrics::new(self.thread_name_prefix))
213 }
214
215 pub fn is_initialized(&self) -> bool {
217 self.pool.get().is_some()
218 }
219
220 pub const fn num_threads(&self) -> usize {
222 self.num_threads
223 }
224
225 pub fn current_num_threads(&self) -> usize {
227 self.pool().current_num_threads()
228 }
229
230 pub fn broadcast(&self, num_threads: usize, f: impl Fn(&mut Worker) + Sync) {
236 if num_threads >= self.pool().current_num_threads() {
237 self.pool().broadcast(|_| {
239 WORKER.with_borrow_mut(|worker| f(worker));
240 });
241 } else {
242 let remaining = AtomicUsize::new(num_threads);
243 self.pool().broadcast(|_| {
244 let mut current = remaining.load(Ordering::Relaxed);
246 loop {
247 if current == 0 {
248 return;
249 }
250 match remaining.compare_exchange_weak(
251 current,
252 current - 1,
253 Ordering::Relaxed,
254 Ordering::Relaxed,
255 ) {
256 Ok(_) => break,
257 Err(actual) => current = actual,
258 }
259 }
260 WORKER.with_borrow_mut(|worker| f(worker));
261 });
262 }
263 }
264
265 pub fn install<R: Send>(&self, f: impl FnOnce(&Worker) -> R + Send) -> R {
271 let pool = self.pool();
272 let metrics = self.metrics().clone();
273 let queued_at = Instant::now();
274
275 pool.install(move || {
276 let started_at = Instant::now();
277 metrics.record_job_queue_wait(started_at.saturating_duration_since(queued_at));
278 let _record_job_duration = RecordWorkerPoolJobDurationOnDrop::new(metrics, started_at);
279 WORKER.with_borrow(|worker| f(worker))
280 })
281 }
282
283 pub fn install_fn<R: Send>(&self, f: impl FnOnce() -> R + Send) -> R {
288 let pool = self.pool();
289 let metrics = self.metrics().clone();
290 let queued_at = Instant::now();
291
292 pool.install(move || {
293 let started_at = Instant::now();
294 metrics.record_job_queue_wait(started_at.saturating_duration_since(queued_at));
295 let _record_job_duration = RecordWorkerPoolJobDurationOnDrop::new(metrics, started_at);
296 f()
297 })
298 }
299
300 pub fn spawn_and_wait<R: Send + 'static>(&self, f: impl FnOnce() -> R + Send + 'static) -> R {
306 let pool = self.pool();
307 if pool.current_thread_index().is_some() {
308 return f()
309 }
310
311 let (tx, rx) = std::sync::mpsc::sync_channel(1);
312 self.spawn(move || {
313 let _ = tx.send(catch_unwind(AssertUnwindSafe(f)));
314 });
315
316 match rx.recv().expect("worker pool exited before completing task") {
317 Ok(result) => result,
318 Err(panic) => std::panic::resume_unwind(panic),
319 }
320 }
321
322 pub fn spawn(&self, f: impl FnOnce() + Send + 'static) {
324 let pool = self.pool();
325 let metrics = self.metrics().clone();
326 let queued_at = Instant::now();
327
328 pool.spawn(move || {
329 let started_at = Instant::now();
330 metrics.record_job_queue_wait(started_at.saturating_duration_since(queued_at));
331 let _record_job_duration = RecordWorkerPoolJobDurationOnDrop::new(metrics, started_at);
332 f();
333 });
334 }
335
336 pub fn in_place_scope<'scope, R>(&self, f: impl FnOnce(&rayon::Scope<'scope>) -> R) -> R {
340 self.pool().in_place_scope(f)
341 }
342
343 pub fn with_worker<R>(f: impl FnOnce(&Worker) -> R) -> R {
362 WORKER.with_borrow(|worker| f(worker))
363 }
364
365 pub fn with_worker_mut<R>(f: impl FnOnce(&mut Worker) -> R) -> R {
384 WORKER.with_borrow_mut(|worker| f(worker))
385 }
386}
387
388struct RecordWorkerPoolJobDurationOnDrop {
390 metrics: WorkerPoolMetrics,
391 started_at: Instant,
392}
393
394impl RecordWorkerPoolJobDurationOnDrop {
395 const fn new(metrics: WorkerPoolMetrics, started_at: Instant) -> Self {
396 Self { metrics, started_at }
397 }
398}
399
400impl Drop for RecordWorkerPoolJobDurationOnDrop {
401 fn drop(&mut self) {
402 self.metrics.record_job_duration(self.started_at.elapsed());
403 }
404}
405
406pub fn build_pool_with_panic_handler(
411 builder: rayon::ThreadPoolBuilder,
412) -> Result<rayon::ThreadPool, rayon::ThreadPoolBuildError> {
413 builder.panic_handler(|_| {}).build()
414}
415
416#[derive(Debug, Default)]
421pub struct Worker {
422 state: Option<(Weak<WorkerStatefulPool>, Box<dyn Any>)>,
423}
424
425impl Worker {
426 const fn new() -> Self {
428 Self { state: None }
429 }
430
431 fn init<T: 'static>(
437 &mut self,
438 stateful_pool: &Weak<WorkerStatefulPool>,
439 f: impl FnOnce(Option<&mut T>) -> T,
440 ) {
441 let existing = self.state.take().and_then(|(owner, boxed)| {
442 (Weak::ptr_eq(&owner, stateful_pool) && boxed.is::<T>()).then_some(boxed)
443 });
444
445 let new_state = match existing {
446 Some(mut boxed) => {
447 let r = boxed.downcast_mut::<T>().expect("type checked above");
448 *r = f(Some(r));
449 boxed
450 }
451 None => Box::new(f(None)),
452 };
453
454 self.state = Some((stateful_pool.clone(), new_state));
455 }
456
457 fn get<T: 'static>(&self, stateful_pool: &Weak<WorkerStatefulPool>) -> &T {
463 let (owner, state) = self.state.as_ref().expect("worker not initialized");
464 assert!(Weak::ptr_eq(owner, stateful_pool), "worker state run mismatch");
465 state.downcast_ref::<T>().expect("worker state type mismatch")
466 }
467
468 fn get_mut<T: 'static>(&mut self, stateful_pool: &Weak<WorkerStatefulPool>) -> &mut T {
474 let (owner, state) = self.state.as_mut().expect("worker not initialized");
475 assert!(Weak::ptr_eq(owner, stateful_pool), "worker state run mismatch");
476 state.downcast_mut::<T>().expect("worker state type mismatch")
477 }
478
479 fn get_or_init<T: 'static>(
485 &mut self,
486 stateful_pool: &Weak<WorkerStatefulPool>,
487 f: impl FnOnce() -> T,
488 ) -> &mut T {
489 if !self
490 .state
491 .as_ref()
492 .is_some_and(|(owner, state)| Weak::ptr_eq(owner, stateful_pool) && state.is::<T>())
493 {
494 self.state = None;
495 self.state = Some((stateful_pool.clone(), Box::new(f())));
496 }
497 self.get_mut(stateful_pool)
498 }
499
500 fn clear(&mut self, stateful_pool: &Weak<WorkerStatefulPool>) {
502 if self.state.as_ref().is_some_and(|(owner, _)| Weak::ptr_eq(owner, stateful_pool)) {
503 self.state = None;
504 }
505 }
506}
507
508#[derive(Debug)]
519pub struct WorkerStatefulPool {
520 pool: Arc<WorkerPool>,
521 owner: Weak<Self>,
522}
523
524impl WorkerStatefulPool {
525 pub fn new(pool: Arc<WorkerPool>) -> Arc<Self> {
527 Arc::new_cyclic(|owner| Self { pool, owner: owner.clone() })
528 }
529
530 pub fn init<T: 'static>(&self, f: impl Fn(Option<&mut T>) -> T + Sync) {
535 self.pool.broadcast(self.pool.current_num_threads(), |worker| {
536 worker.init(&self.owner, &f);
537 });
538 }
539
540 pub fn get<T: 'static, R>(&self, f: impl FnOnce(&T) -> R) -> R {
546 WorkerPool::with_worker(|worker| f(worker.get(&self.owner)))
547 }
548
549 pub fn get_or_init<T: 'static, R>(
558 &self,
559 init: impl FnOnce() -> T,
560 f: impl FnOnce(&mut T) -> R,
561 ) -> R {
562 WorkerPool::with_worker_mut(|worker| f(worker.get_or_init(&self.owner, init)))
563 }
564}
565
566impl Deref for WorkerStatefulPool {
567 type Target = WorkerPool;
568
569 fn deref(&self) -> &Self::Target {
570 &self.pool
571 }
572}
573
574impl Drop for WorkerStatefulPool {
575 fn drop(&mut self) {
576 if let Some(pool) = self.pool.pool.get() {
577 pool.broadcast(|_| {
578 WORKER.with_borrow_mut(|worker| worker.clear(&self.owner));
579 });
580 }
581 }
582}
583
584#[cfg(test)]
585mod tests {
586 use super::*;
587 use rayon::prelude::*;
588 use std::time::Duration;
589
590 #[tokio::test]
591 async fn blocking_pool() {
592 let pool = BlockingTaskPool::build().unwrap();
593 let res = pool.spawn(move || 5);
594 let res = res.await.unwrap();
595 assert_eq!(res, 5);
596 }
597
598 #[tokio::test]
599 async fn blocking_pool_panic() {
600 let pool = BlockingTaskPool::build().unwrap();
601 let res = pool.spawn(move || -> i32 {
602 panic!();
603 });
604 let res = res.await;
605 assert!(res.is_err());
606 }
607
608 #[test]
609 fn worker_pool_init_and_access() {
610 let pool = Arc::new(WorkerPool::new(2, "test"));
611 let state = WorkerStatefulPool::new(Arc::clone(&pool));
612 state.init(|_| vec![1u8, 2, 3]);
613 let sum = pool.install_fn(|| state.get(|v: &Vec<u8>| v.iter().sum::<u8>()));
614 assert_eq!(sum, 6);
615 drop(state);
616 pool.broadcast(2, |worker| assert!(worker.state.is_none()));
617 }
618
619 #[test]
620 fn worker_pool_reinit_reuses_resources() {
621 let pool = Arc::new(WorkerPool::new(1, "test"));
622 let state = WorkerStatefulPool::new(Arc::clone(&pool));
623 state.init(|existing: Option<&mut Vec<u8>>| {
624 assert!(existing.is_none());
625 vec![1, 2, 3]
626 });
627 state.init(|existing: Option<&mut Vec<u8>>| {
628 let v = existing.expect("should have existing state");
629 assert_eq!(v, &mut vec![1, 2, 3]);
630 v.push(4);
631 std::mem::take(v)
632 });
633 let len = pool.install_fn(|| state.get(Vec::<u8>::len));
634 assert_eq!(len, 4);
635 }
636
637 #[test]
638 fn worker_pool_clear_and_reinit() {
639 let pool = Arc::new(WorkerPool::new(1, "test"));
640 let state = WorkerStatefulPool::new(Arc::clone(&pool));
641 state.init(|_| 42u64);
642 assert_eq!(pool.install_fn(|| state.get(|v: &u64| *v)), 42);
643 drop(state);
644 let state = WorkerStatefulPool::new(Arc::clone(&pool));
645 state.init(|_| String::from("hello"));
646 assert_eq!(pool.install_fn(|| state.get(String::clone)), "hello");
647 }
648
649 #[test]
650 fn worker_pool_par_iter_with_worker() {
651 let pool = Arc::new(WorkerPool::new(2, "test"));
652 let state = WorkerStatefulPool::new(Arc::clone(&pool));
653 state.init(|_| 10u64);
654 let results = pool.install_fn(|| {
655 (0u64..4).into_par_iter().map(|i| state.get(|v: &u64| i + *v)).collect::<Vec<_>>()
656 });
657 assert_eq!(results, vec![10, 11, 12, 13]);
658 }
659
660 #[test]
661 fn worker_pool_spawn_and_wait_runs_on_target_pool() {
662 let caller_pool = WorkerPool::new(1, "caller");
663 let target_pool = Arc::new(WorkerPool::new(1, "target"));
664
665 let thread_name = caller_pool.install_fn({
666 let target_pool = Arc::clone(&target_pool);
667 move || {
668 WorkerPool::with_worker_mut(|_| {
669 target_pool.spawn_and_wait(|| thread::current().name().unwrap().to_owned())
670 })
671 }
672 });
673
674 assert_eq!(thread_name, "target-00");
675 }
676
677 #[test]
678 fn worker_get_or_init_reuses_only_matching_run_and_type() {
679 let mut worker = Worker::new();
680 let first = stateful_pool();
681 let second = stateful_pool();
682
683 worker.get_or_init(&first.owner, || vec![1u8]).push(2);
684 assert_eq!(
685 worker.get_or_init::<Vec<u8>>(&first.owner.clone(), || panic!("must reuse")),
686 &[1, 2]
687 );
688 assert_eq!(worker.get_or_init(&second.owner, || vec![3u8]), &[3]);
689 assert_eq!(worker.get_or_init(&second.owner, || String::from("new type")), "new type");
690 assert_eq!(*worker.get_or_init(&first.owner, || 42u64), 42);
691 }
692
693 #[test]
694 fn worker_init_reuses_only_matching_run_and_type() {
695 let mut worker = Worker::new();
696 let first = stateful_pool();
697 let second = stateful_pool();
698 worker.init(&first.owner, |_| 1u64);
699 worker.init(&second.owner, |existing: Option<&mut u64>| {
700 assert!(existing.is_none());
701 2
702 });
703 worker.init(&second.owner, |existing: Option<&mut String>| {
704 assert!(existing.is_none());
705 String::from("new type")
706 });
707 assert_eq!(worker.get::<String>(&second.owner), "new type");
708 }
709
710 #[test]
711 fn worker_pool_late_cleanup_preserves_other_runs() {
712 let pool = Arc::new(WorkerPool::new(2, "run-cleanup"));
713 let first = WorkerStatefulPool::new(Arc::clone(&pool));
714 let second = WorkerStatefulPool::new(Arc::clone(&pool));
715 first.init(|_| 1u64);
716 pool.broadcast(1, |worker| {
717 assert_eq!(*worker.get_or_init(&second.owner, || 2u64), 2);
718 });
719 drop(first);
720 let retained = AtomicUsize::new(0);
721 pool.broadcast(2, |worker| {
722 if let Some((owner, state)) = &worker.state {
723 assert!(Weak::ptr_eq(owner, &second.owner));
724 assert_eq!(state.downcast_ref::<u64>(), Some(&2));
725 retained.fetch_add(1, Ordering::Relaxed);
726 }
727 });
728 assert_eq!(retained.load(Ordering::Relaxed), 1);
729 drop(second);
730 pool.broadcast(2, |worker| assert!(worker.state.is_none()));
731 }
732
733 #[test]
734 #[should_panic(expected = "worker state run mismatch")]
735 fn worker_get_rejects_another_run() {
736 let mut worker = Worker::new();
737 worker.init(&stateful_pool().owner, |_| 1u64);
738 worker.get::<u64>(&stateful_pool().owner);
739 }
740
741 #[test]
742 #[should_panic(expected = "worker state run mismatch")]
743 fn worker_get_mut_rejects_another_run() {
744 let mut worker = Worker::new();
745 worker.init(&stateful_pool().owner, |_| 1u64);
746 worker.get_mut::<u64>(&stateful_pool().owner);
747 }
748
749 #[test]
750 fn worker_replacement_drops_previous_state_before_initialization() {
751 let mut worker = Worker::new();
752 let previous_run = stateful_pool();
753 let next_run = stateful_pool();
754 let resource = Arc::new(());
755
756 for owner in [&previous_run.owner, &next_run.owner] {
757 worker.init(&previous_run.owner, |_| Arc::clone(&resource));
758 assert_eq!(Arc::strong_count(&resource), 2);
759 assert_eq!(
760 *worker.get_or_init(owner, || {
761 assert_eq!(Arc::strong_count(&resource), 1);
762 42u64
763 }),
764 42,
765 );
766 }
767 }
768
769 #[test]
770 fn worker_failed_replacement_leaves_state_uninitialized() {
771 let mut worker = Worker::new();
772 let previous_run = stateful_pool();
773 let next_run = stateful_pool();
774 worker.init(&previous_run.owner, |_| 1u64);
775
776 let panic = catch_unwind(AssertUnwindSafe(|| {
777 worker.get_or_init::<u64>(&next_run.owner, || panic!("initialization failed"));
778 }))
779 .unwrap_err();
780 assert_eq!(panic.downcast_ref::<&str>(), Some(&"initialization failed"));
781 assert!(worker.state.is_none());
782 assert_eq!(*worker.get_or_init(&next_run.owner, || 2u64), 2);
783 }
784
785 #[test]
786 fn stateful_pool_drop_does_not_initialize_unused_pool() {
787 let pool = Arc::new(WorkerPool::new(1, "unused-state"));
788 drop(WorkerStatefulPool::new(Arc::clone(&pool)));
789 assert!(!pool.is_initialized());
790 }
791
792 #[test]
793 fn stateful_pool_last_job_handle_clears_state() {
794 let pool = Arc::new(WorkerPool::new(2, "owned-state"));
795 let state = WorkerStatefulPool::new(Arc::clone(&pool));
796 let owner = Arc::downgrade(&state);
797 let resource = Arc::new(());
798 state.init(|_| Arc::clone(&resource));
799 let (ready_tx, ready_rx) = std::sync::mpsc::channel();
800 let (release_tx, release_rx) = std::sync::mpsc::channel();
801 let (done_tx, done_rx) = std::sync::mpsc::channel();
802 let job_state = Arc::clone(&state);
803 state.spawn(move || {
804 ready_tx.send(()).unwrap();
805 release_rx.recv_timeout(Duration::from_secs(5)).unwrap();
806 job_state.get(|value: &Arc<()>| assert_eq!(Arc::strong_count(value), 3));
807 drop(job_state);
808 done_tx.send(()).unwrap();
809 });
810 ready_rx.recv_timeout(Duration::from_secs(5)).unwrap();
811 drop(state);
812 assert!(owner.upgrade().is_some());
813 assert_eq!(Arc::strong_count(&resource), 3);
814 release_tx.send(()).unwrap();
815 done_rx.recv_timeout(Duration::from_secs(5)).unwrap();
816 assert!(owner.upgrade().is_none());
817 assert_eq!(Arc::strong_count(&resource), 1);
818 pool.broadcast(2, |worker| assert!(worker.state.is_none()));
819 }
820
821 #[test]
822 fn stateful_pool_clears_after_job_panics() {
823 let pool = Arc::new(WorkerPool::new(1, "unwind-state"));
824 let state = WorkerStatefulPool::new(Arc::clone(&pool));
825 let resource = Arc::new(());
826 state.init(|_| Arc::clone(&resource));
827 let panic = catch_unwind(AssertUnwindSafe(|| {
828 pool.install_fn(move || {
829 state.get_or_init(|| Arc::new(()), |_| panic!("job failed"));
830 });
831 }))
832 .unwrap_err();
833 assert_eq!(panic.downcast_ref::<&str>(), Some(&"job failed"));
834 assert_eq!(Arc::strong_count(&resource), 1);
835 pool.broadcast(1, |worker| assert!(worker.state.is_none()));
836 }
837
838 #[test]
839 fn stateful_pool_clears_after_unwrapping_owner() {
840 let pool = Arc::new(WorkerPool::new(1, "unwrapped-state"));
841 let state = WorkerStatefulPool::new(Arc::clone(&pool));
842 let resource = Arc::new(());
843 state.init(|_| Arc::clone(&resource));
844 let state = Arc::try_unwrap(state).unwrap();
845 assert_eq!(Arc::strong_count(&resource), 2);
846 drop(state);
847 assert_eq!(Arc::strong_count(&resource), 1);
848 pool.broadcast(1, |worker| assert!(worker.state.is_none()));
849 }
850
851 fn stateful_pool() -> Arc<WorkerStatefulPool> {
852 WorkerStatefulPool::new(Arc::new(WorkerPool::new(1, "state-test")))
853 }
854}