1use crate::metrics::WorkerPoolMetrics;
4use std::{
5 any::Any,
6 cell::RefCell,
7 future::Future,
8 panic::{catch_unwind, AssertUnwindSafe},
9 pin::Pin,
10 sync::{
11 atomic::{AtomicUsize, Ordering},
12 Arc, OnceLock,
13 },
14 task::{ready, Context, Poll},
15 thread,
16 time::Instant,
17};
18use tokio::sync::{oneshot, AcquireError, OwnedSemaphorePermit, Semaphore};
19
20#[derive(Clone, Debug)]
29pub struct BlockingTaskGuard(Arc<Semaphore>);
30
31impl BlockingTaskGuard {
32 pub fn new(max_blocking_tasks: usize) -> Self {
35 Self(Arc::new(Semaphore::new(max_blocking_tasks)))
36 }
37
38 pub async fn acquire_owned(self) -> Result<OwnedSemaphorePermit, AcquireError> {
40 self.0.acquire_owned().await
41 }
42
43 pub async fn acquire_many_owned(self, n: u32) -> Result<OwnedSemaphorePermit, AcquireError> {
45 self.0.acquire_many_owned(n).await
46 }
47}
48
49#[derive(Clone, Debug)]
62pub struct BlockingTaskPool {
63 pool: Arc<rayon::ThreadPool>,
64}
65
66impl BlockingTaskPool {
67 pub fn new(pool: rayon::ThreadPool) -> Self {
69 Self { pool: Arc::new(pool) }
70 }
71
72 pub fn builder() -> rayon::ThreadPoolBuilder {
74 rayon::ThreadPoolBuilder::new()
75 }
76
77 pub fn build() -> Result<Self, rayon::ThreadPoolBuildError> {
83 Self::builder().build().map(Self::new)
84 }
85
86 pub fn spawn<F, R>(&self, func: F) -> BlockingTaskHandle<R>
94 where
95 F: FnOnce() -> R + Send + 'static,
96 R: Send + 'static,
97 {
98 let (tx, rx) = oneshot::channel();
99
100 self.pool.spawn(move || {
101 let _result = tx.send(catch_unwind(AssertUnwindSafe(func)));
102 });
103
104 BlockingTaskHandle { rx }
105 }
106
107 pub fn spawn_fifo<F, R>(&self, func: F) -> BlockingTaskHandle<R>
115 where
116 F: FnOnce() -> R + Send + 'static,
117 R: Send + 'static,
118 {
119 let (tx, rx) = oneshot::channel();
120
121 self.pool.spawn_fifo(move || {
122 let _result = tx.send(catch_unwind(AssertUnwindSafe(func)));
123 });
124
125 BlockingTaskHandle { rx }
126 }
127}
128
129#[derive(Debug)]
135#[must_use = "futures do nothing unless you `.await` or poll them"]
136#[pin_project::pin_project]
137pub struct BlockingTaskHandle<T> {
138 #[pin]
139 pub(crate) rx: oneshot::Receiver<thread::Result<T>>,
140}
141
142impl<T> Future for BlockingTaskHandle<T> {
143 type Output = thread::Result<T>;
144
145 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
146 match ready!(self.project().rx.poll(cx)) {
147 Ok(res) => Poll::Ready(res),
148 Err(_) => Poll::Ready(Err(Box::<TokioBlockingTaskError>::default())),
149 }
150 }
151}
152
153#[derive(Debug, Default, thiserror::Error)]
157#[error("tokio channel dropped while awaiting result")]
158#[non_exhaustive]
159pub struct TokioBlockingTaskError;
160
161thread_local! {
162 static WORKER: RefCell<Worker> = const { RefCell::new(Worker::new()) };
163}
164
165#[derive(Debug)]
176pub struct WorkerPool {
177 pool: OnceLock<rayon::ThreadPool>,
178 metrics: OnceLock<WorkerPoolMetrics>,
179 num_threads: usize,
180 thread_name_prefix: &'static str,
181}
182
183impl WorkerPool {
184 pub const fn new(num_threads: usize, thread_name_prefix: &'static str) -> Self {
189 Self { pool: OnceLock::new(), metrics: OnceLock::new(), num_threads, thread_name_prefix }
190 }
191
192 fn pool(&self) -> &rayon::ThreadPool {
194 self.pool.get_or_init(|| {
195 let prefix = self.thread_name_prefix;
196 build_pool_with_panic_handler(
197 rayon::ThreadPoolBuilder::new()
198 .num_threads(self.num_threads)
199 .thread_name(move |i| format!("{prefix}-{i:02}")),
200 )
201 .unwrap_or_else(|err| panic!("failed to build {prefix} worker pool: {err}"))
202 })
203 }
204
205 fn metrics(&self) -> &WorkerPoolMetrics {
207 self.metrics.get_or_init(|| WorkerPoolMetrics::new(self.thread_name_prefix))
208 }
209
210 pub fn is_initialized(&self) -> bool {
212 self.pool.get().is_some()
213 }
214
215 pub fn current_num_threads(&self) -> usize {
217 self.pool().current_num_threads()
218 }
219
220 pub fn init<T: 'static>(&self, f: impl Fn(Option<&mut T>) -> T + Sync) {
222 self.broadcast(self.pool().current_num_threads(), |worker| {
223 worker.init::<T>(&f);
224 });
225 }
226
227 pub fn broadcast(&self, num_threads: usize, f: impl Fn(&mut Worker) + Sync) {
233 if num_threads >= self.pool().current_num_threads() {
234 self.pool().broadcast(|_| {
236 WORKER.with_borrow_mut(|worker| f(worker));
237 });
238 } else {
239 let remaining = AtomicUsize::new(num_threads);
240 self.pool().broadcast(|_| {
241 let mut current = remaining.load(Ordering::Relaxed);
243 loop {
244 if current == 0 {
245 return;
246 }
247 match remaining.compare_exchange_weak(
248 current,
249 current - 1,
250 Ordering::Relaxed,
251 Ordering::Relaxed,
252 ) {
253 Ok(_) => break,
254 Err(actual) => current = actual,
255 }
256 }
257 WORKER.with_borrow_mut(|worker| f(worker));
258 });
259 }
260 }
261
262 pub fn clear(&self) {
264 self.pool().broadcast(|_| {
265 WORKER.with_borrow_mut(Worker::clear);
266 });
267 }
268
269 pub fn install<R: Send>(&self, f: impl FnOnce(&Worker) -> R + Send) -> R {
275 let pool = self.pool();
276 let metrics = self.metrics().clone();
277 let queued_at = Instant::now();
278
279 pool.install(move || {
280 let started_at = Instant::now();
281 metrics.record_job_queue_wait(started_at.saturating_duration_since(queued_at));
282 let _record_job_duration = RecordWorkerPoolJobDurationOnDrop::new(metrics, started_at);
283 WORKER.with_borrow(|worker| f(worker))
284 })
285 }
286
287 pub fn install_fn<R: Send>(&self, f: impl FnOnce() -> R + Send) -> R {
292 let pool = self.pool();
293 let metrics = self.metrics().clone();
294 let queued_at = Instant::now();
295
296 pool.install(move || {
297 let started_at = Instant::now();
298 metrics.record_job_queue_wait(started_at.saturating_duration_since(queued_at));
299 let _record_job_duration = RecordWorkerPoolJobDurationOnDrop::new(metrics, started_at);
300 f()
301 })
302 }
303
304 pub fn spawn(&self, f: impl FnOnce() + Send + 'static) {
306 let pool = self.pool();
307 let metrics = self.metrics().clone();
308 let queued_at = Instant::now();
309
310 pool.spawn(move || {
311 let started_at = Instant::now();
312 metrics.record_job_queue_wait(started_at.saturating_duration_since(queued_at));
313 let _record_job_duration = RecordWorkerPoolJobDurationOnDrop::new(metrics, started_at);
314 f();
315 });
316 }
317
318 pub fn in_place_scope<'scope, R>(&self, f: impl FnOnce(&rayon::Scope<'scope>) -> R) -> R {
322 self.pool().in_place_scope(f)
323 }
324
325 pub fn with_worker<R>(f: impl FnOnce(&Worker) -> R) -> R {
330 WORKER.with_borrow(|worker| f(worker))
331 }
332
333 pub fn with_worker_mut<R>(f: impl FnOnce(&mut Worker) -> R) -> R {
335 WORKER.with_borrow_mut(|worker| f(worker))
336 }
337}
338
339struct RecordWorkerPoolJobDurationOnDrop {
341 metrics: WorkerPoolMetrics,
342 started_at: Instant,
343}
344
345impl RecordWorkerPoolJobDurationOnDrop {
346 const fn new(metrics: WorkerPoolMetrics, started_at: Instant) -> Self {
347 Self { metrics, started_at }
348 }
349}
350
351impl Drop for RecordWorkerPoolJobDurationOnDrop {
352 fn drop(&mut self) {
353 self.metrics.record_job_duration(self.started_at.elapsed());
354 }
355}
356
357pub fn build_pool_with_panic_handler(
362 builder: rayon::ThreadPoolBuilder,
363) -> Result<rayon::ThreadPool, rayon::ThreadPoolBuildError> {
364 builder.panic_handler(|_| {}).build()
365}
366
367#[derive(Debug, Default)]
372pub struct Worker {
373 state: Option<Box<dyn Any>>,
374}
375
376impl Worker {
377 const fn new() -> Self {
379 Self { state: None }
380 }
381
382 pub fn init<T: 'static>(&mut self, f: impl FnOnce(Option<&mut T>) -> T) {
387 let existing =
388 self.state.take().and_then(|mut b| b.downcast_mut::<T>().is_some().then_some(b));
389
390 let new_state = match existing {
391 Some(mut boxed) => {
392 let r = boxed.downcast_mut::<T>().expect("type checked above");
393 *r = f(Some(r));
394 boxed
395 }
396 None => Box::new(f(None)),
397 };
398
399 self.state = Some(new_state);
400 }
401
402 pub fn get<T: 'static>(&self) -> &T {
408 self.state
409 .as_ref()
410 .expect("worker not initialized")
411 .downcast_ref::<T>()
412 .expect("worker state type mismatch")
413 }
414
415 pub fn get_mut<T: 'static>(&mut self) -> &mut T {
421 self.state
422 .as_mut()
423 .expect("worker not initialized")
424 .downcast_mut::<T>()
425 .expect("worker state type mismatch")
426 }
427
428 pub fn get_or_init<T: 'static>(&mut self, f: impl FnOnce() -> T) -> &mut T {
434 self.state
435 .get_or_insert_with(|| Box::new(f()))
436 .downcast_mut::<T>()
437 .expect("worker state type mismatch")
438 }
439
440 pub fn clear(&mut self) {
442 self.state = None;
443 }
444}
445
446#[cfg(test)]
447mod tests {
448 use super::*;
449
450 #[tokio::test]
451 async fn blocking_pool() {
452 let pool = BlockingTaskPool::build().unwrap();
453 let res = pool.spawn(move || 5);
454 let res = res.await.unwrap();
455 assert_eq!(res, 5);
456 }
457
458 #[tokio::test]
459 async fn blocking_pool_panic() {
460 let pool = BlockingTaskPool::build().unwrap();
461 let res = pool.spawn(move || -> i32 {
462 panic!();
463 });
464 let res = res.await;
465 assert!(res.is_err());
466 }
467
468 #[test]
469 fn worker_pool_init_and_access() {
470 let pool = WorkerPool::new(2, "test");
471
472 pool.broadcast(2, |worker| {
473 worker.init::<Vec<u8>>(|_| vec![1, 2, 3]);
474 });
475
476 let sum: u8 = pool.install(|worker| {
477 let v = worker.get::<Vec<u8>>();
478 v.iter().sum()
479 });
480 assert_eq!(sum, 6);
481
482 pool.clear();
483 }
484
485 #[test]
486 fn worker_pool_reinit_reuses_resources() {
487 let pool = WorkerPool::new(1, "test");
488
489 pool.broadcast(1, |worker| {
490 worker.init::<Vec<u8>>(|existing| {
491 assert!(existing.is_none());
492 vec![1, 2, 3]
493 });
494 });
495
496 pool.broadcast(1, |worker| {
497 worker.init::<Vec<u8>>(|existing| {
498 let v = existing.expect("should have existing state");
499 assert_eq!(v, &mut vec![1, 2, 3]);
500 v.push(4);
501 std::mem::take(v)
502 });
503 });
504
505 let len = pool.install(|worker| worker.get::<Vec<u8>>().len());
506 assert_eq!(len, 4);
507
508 pool.clear();
509 }
510
511 #[test]
512 fn worker_pool_clear_and_reinit() {
513 let pool = WorkerPool::new(1, "test");
514
515 pool.broadcast(1, |worker| {
516 worker.init::<u64>(|_| 42);
517 });
518 let val = pool.install(|worker| *worker.get::<u64>());
519 assert_eq!(val, 42);
520
521 pool.clear();
522
523 pool.broadcast(1, |worker| {
524 worker.init::<String>(|_| "hello".to_string());
525 });
526 let val = pool.install(|worker| worker.get::<String>().clone());
527 assert_eq!(val, "hello");
528
529 pool.clear();
530 }
531
532 #[test]
533 fn worker_pool_par_iter_with_worker() {
534 use rayon::prelude::*;
535
536 let pool = WorkerPool::new(2, "test");
537
538 pool.broadcast(2, |worker| {
539 worker.init::<u64>(|_| 10);
540 });
541
542 let results: Vec<u64> = pool.install(|_| {
543 (0u64..4)
544 .into_par_iter()
545 .map(|i| WorkerPool::with_worker(|w| i + *w.get::<u64>()))
546 .collect()
547 });
548 assert_eq!(results, vec![10, 11, 12, 13]);
549
550 pool.clear();
551 }
552}