qubit_execution_services/execution_services.rs
1/*******************************************************************************
2 *
3 * Copyright (c) 2025 - 2026 Haixing Hu.
4 *
5 * SPDX-License-Identifier: Apache-2.0
6 *
7 * Licensed under the Apache License, Version 2.0.
8 *
9 ******************************************************************************/
10use std::{
11 future::Future,
12 pin::Pin,
13 sync::Arc,
14 time::Duration,
15};
16
17use qubit_executor::{
18 TaskHandle,
19 TrackedTask,
20};
21use qubit_function::{
22 Callable,
23 Runnable,
24};
25use qubit_thread_pool::{
26 ThreadPool,
27 ThreadPoolBuilder,
28};
29use qubit_tokio_executor::TokioExecutorService;
30
31use super::{
32 ExecutionServicesBuildError,
33 ExecutionServicesBuilder,
34 ExecutionServicesStopReport,
35 ExecutorService,
36 ExecutorServiceLifecycle,
37 RayonExecutorService,
38 RayonTaskHandle,
39 SubmissionError,
40 TokioBlockingTaskHandle,
41 TokioIoExecutorService,
42 TokioTaskHandle,
43};
44
45/// Default managed service for synchronous tasks that may block an OS thread.
46pub type BlockingExecutorService = ThreadPool;
47
48/// Builder alias for configuring [`BlockingExecutorService`].
49pub type BlockingExecutorServiceBuilder = ThreadPoolBuilder;
50
51/// Tokio-backed blocking executor service routed through `spawn_blocking`.
52pub type TokioBlockingExecutorService = TokioExecutorService;
53
54/// Unified facade exposing separate execution domains through one owner.
55///
56/// The facade does not implement a single scheduling core. Instead it routes
57/// work to one of four dedicated execution domains:
58///
59/// - `blocking`: synchronous tasks that may block an OS thread.
60/// - `cpu`: CPU-bound synchronous tasks backed by Rayon.
61/// - `tokio_blocking`: blocking tasks routed through Tokio `spawn_blocking`.
62/// - `io`: async futures spawned on Tokio's async runtime.
63pub struct ExecutionServices {
64 /// Managed service for synchronous tasks that may block OS threads.
65 blocking: Arc<BlockingExecutorService>,
66 /// Managed service for CPU-bound synchronous tasks.
67 cpu: RayonExecutorService,
68 /// Tokio-backed blocking service using `spawn_blocking`.
69 tokio_blocking: TokioBlockingExecutorService,
70 /// Tokio-backed async service for Future-based tasks.
71 io: TokioIoExecutorService,
72}
73
74impl ExecutionServices {
75 /// Creates an execution-services facade from its four execution domains.
76 ///
77 /// # Parameters
78 ///
79 /// * `blocking` - Blocking executor domain.
80 /// * `cpu` - CPU-bound executor domain.
81 /// * `tokio_blocking` - Tokio blocking executor domain.
82 /// * `io` - Tokio async IO executor domain.
83 ///
84 /// # Returns
85 ///
86 /// An execution-services facade owning all supplied domains.
87 pub(crate) fn from_parts(
88 blocking: BlockingExecutorService,
89 cpu: RayonExecutorService,
90 tokio_blocking: TokioBlockingExecutorService,
91 io: TokioIoExecutorService,
92 ) -> Self {
93 Self {
94 blocking: Arc::new(blocking),
95 cpu,
96 tokio_blocking,
97 io,
98 }
99 }
100
101 /// Creates an execution-services facade with default builder settings.
102 ///
103 /// # Returns
104 ///
105 /// `Ok(ExecutionServices)` if the default blocking and CPU domains build
106 /// successfully.
107 ///
108 /// # Errors
109 ///
110 /// Returns [`ExecutionServicesBuildError`] if the default builder
111 /// configuration is rejected.
112 #[inline]
113 pub fn new() -> Result<Self, ExecutionServicesBuildError> {
114 Self::builder().build()
115 }
116
117 /// Creates a builder for configuring the execution-services facade.
118 ///
119 /// # Returns
120 ///
121 /// A builder configured with CPU-parallelism defaults.
122 #[inline]
123 pub fn builder() -> ExecutionServicesBuilder {
124 ExecutionServicesBuilder::default()
125 }
126
127 /// Returns the blocking execution domain.
128 ///
129 /// # Returns
130 ///
131 /// A shared reference to the blocking executor service.
132 #[inline]
133 pub fn blocking(&self) -> &BlockingExecutorService {
134 self.blocking.as_ref()
135 }
136
137 /// Returns the CPU execution domain.
138 ///
139 /// # Returns
140 ///
141 /// A shared reference to the Rayon-backed CPU executor service.
142 #[inline]
143 pub fn cpu(&self) -> &RayonExecutorService {
144 &self.cpu
145 }
146
147 /// Returns the Tokio blocking execution domain.
148 ///
149 /// # Returns
150 ///
151 /// A shared reference to the Tokio blocking executor service.
152 #[inline]
153 pub fn tokio_blocking(&self) -> &TokioBlockingExecutorService {
154 &self.tokio_blocking
155 }
156
157 /// Returns the Tokio async IO execution domain.
158 ///
159 /// # Returns
160 ///
161 /// A shared reference to the Tokio IO executor service.
162 #[inline]
163 pub fn io(&self) -> &TokioIoExecutorService {
164 &self.io
165 }
166
167 /// Submits a blocking runnable task to the blocking domain.
168 ///
169 /// # Parameters
170 ///
171 /// * `task` - Runnable task that may block an OS thread.
172 ///
173 /// # Returns
174 ///
175 /// `Ok(())` if the blocking domain accepts the task.
176 ///
177 /// # Errors
178 ///
179 /// Returns [`SubmissionError`] if the blocking domain refuses the task.
180 #[inline]
181 pub fn submit_blocking<T, E>(&self, task: T) -> Result<(), SubmissionError>
182 where
183 T: Runnable<E> + Send + 'static,
184 E: Send + 'static,
185 {
186 self.blocking.submit(task)
187 }
188
189 /// Submits a blocking runnable task and returns a tracked handle.
190 ///
191 /// # Parameters
192 ///
193 /// * `task` - Runnable task that may block an OS thread.
194 ///
195 /// # Returns
196 ///
197 /// A [`TrackedTask`] for the accepted blocking task.
198 ///
199 /// # Errors
200 ///
201 /// Returns [`SubmissionError`] if the blocking domain refuses the task.
202 #[inline]
203 pub fn submit_tracked_blocking<T, E>(&self, task: T) -> Result<TrackedTask<(), E>, SubmissionError>
204 where
205 T: Runnable<E> + Send + 'static,
206 E: Send + 'static,
207 {
208 self.blocking.submit_tracked(task)
209 }
210
211 /// Submits a blocking callable task to the blocking domain.
212 ///
213 /// # Parameters
214 ///
215 /// * `task` - Callable task that may block an OS thread.
216 ///
217 /// # Returns
218 ///
219 /// A [`TaskHandle`] for the accepted blocking task.
220 ///
221 /// # Errors
222 ///
223 /// Returns [`SubmissionError`] if the blocking domain refuses the task.
224 #[inline]
225 pub fn submit_blocking_callable<C, R, E>(&self, task: C) -> Result<TaskHandle<R, E>, SubmissionError>
226 where
227 C: Callable<R, E> + Send + 'static,
228 R: Send + 'static,
229 E: Send + 'static,
230 {
231 self.blocking.submit_callable(task)
232 }
233
234 /// Submits a blocking callable task and returns a tracked handle.
235 ///
236 /// # Parameters
237 ///
238 /// * `task` - Callable task that may block an OS thread.
239 ///
240 /// # Returns
241 ///
242 /// A [`TrackedTask`] for the accepted blocking task.
243 ///
244 /// # Errors
245 ///
246 /// Returns [`SubmissionError`] if the blocking domain refuses the task.
247 #[inline]
248 pub fn submit_tracked_blocking_callable<C, R, E>(&self, task: C) -> Result<TrackedTask<R, E>, SubmissionError>
249 where
250 C: Callable<R, E> + Send + 'static,
251 R: Send + 'static,
252 E: Send + 'static,
253 {
254 self.blocking.submit_tracked_callable(task)
255 }
256
257 /// Submits a CPU-bound runnable task to the Rayon domain.
258 ///
259 /// # Parameters
260 ///
261 /// * `task` - Runnable CPU task.
262 ///
263 /// # Returns
264 ///
265 /// `Ok(())` if the CPU domain accepts the task.
266 ///
267 /// # Errors
268 ///
269 /// Returns [`SubmissionError`] if the CPU domain refuses the task.
270 #[inline]
271 pub fn submit_cpu<T, E>(&self, task: T) -> Result<(), SubmissionError>
272 where
273 T: Runnable<E> + Send + 'static,
274 E: Send + 'static,
275 {
276 self.cpu.submit(task)
277 }
278
279 /// Submits a CPU-bound runnable task and returns a tracked handle.
280 ///
281 /// # Parameters
282 ///
283 /// * `task` - Runnable CPU task.
284 ///
285 /// # Returns
286 ///
287 /// A [`RayonTaskHandle`] for the accepted CPU task.
288 ///
289 /// # Errors
290 ///
291 /// Returns [`SubmissionError`] if the CPU domain refuses the task.
292 #[inline]
293 pub fn submit_tracked_cpu<T, E>(&self, task: T) -> Result<RayonTaskHandle<(), E>, SubmissionError>
294 where
295 T: Runnable<E> + Send + 'static,
296 E: Send + 'static,
297 {
298 self.cpu.submit_tracked(task)
299 }
300
301 /// Submits a CPU-bound callable task to the Rayon domain.
302 ///
303 /// # Parameters
304 ///
305 /// * `task` - Callable CPU task.
306 ///
307 /// # Returns
308 ///
309 /// A [`TaskHandle`] for the accepted CPU task.
310 ///
311 /// # Errors
312 ///
313 /// Returns [`SubmissionError`] if the CPU domain refuses the task.
314 #[inline]
315 pub fn submit_cpu_callable<C, R, E>(&self, task: C) -> Result<TaskHandle<R, E>, SubmissionError>
316 where
317 C: Callable<R, E> + Send + 'static,
318 R: Send + 'static,
319 E: Send + 'static,
320 {
321 self.cpu.submit_callable(task)
322 }
323
324 /// Submits a CPU-bound callable task and returns a tracked handle.
325 ///
326 /// # Parameters
327 ///
328 /// * `task` - Callable CPU task.
329 ///
330 /// # Returns
331 ///
332 /// A [`RayonTaskHandle`] for the accepted CPU task.
333 ///
334 /// # Errors
335 ///
336 /// Returns [`SubmissionError`] if the CPU domain refuses the task.
337 #[inline]
338 pub fn submit_tracked_cpu_callable<C, R, E>(&self, task: C) -> Result<RayonTaskHandle<R, E>, SubmissionError>
339 where
340 C: Callable<R, E> + Send + 'static,
341 R: Send + 'static,
342 E: Send + 'static,
343 {
344 self.cpu.submit_tracked_callable(task)
345 }
346
347 /// Submits a blocking runnable task to Tokio `spawn_blocking`.
348 ///
349 /// # Parameters
350 ///
351 /// * `task` - Runnable task to execute on Tokio's blocking pool.
352 ///
353 /// # Returns
354 ///
355 /// `Ok(())` if the Tokio blocking domain accepts the task.
356 ///
357 /// # Errors
358 ///
359 /// Returns [`SubmissionError`] if the Tokio blocking domain refuses the
360 /// task.
361 #[inline]
362 pub fn submit_tokio_blocking<T, E>(&self, task: T) -> Result<(), SubmissionError>
363 where
364 T: Runnable<E> + Send + 'static,
365 E: Send + 'static,
366 {
367 self.tokio_blocking.submit(task)
368 }
369
370 /// Submits a blocking runnable task to Tokio and returns a tracked handle.
371 ///
372 /// # Parameters
373 ///
374 /// * `task` - Runnable task to execute on Tokio's blocking pool.
375 ///
376 /// # Returns
377 ///
378 /// A [`TokioBlockingTaskHandle`] for the accepted blocking task.
379 ///
380 /// # Errors
381 ///
382 /// Returns [`SubmissionError`] if the Tokio blocking domain refuses the
383 /// task.
384 #[inline]
385 pub fn submit_tracked_tokio_blocking<T, E>(
386 &self,
387 task: T,
388 ) -> Result<TokioBlockingTaskHandle<(), E>, SubmissionError>
389 where
390 T: Runnable<E> + Send + 'static,
391 E: Send + 'static,
392 {
393 self.tokio_blocking.submit_tracked(task)
394 }
395
396 /// Submits a blocking callable task to Tokio `spawn_blocking`.
397 ///
398 /// # Parameters
399 ///
400 /// * `task` - Callable task to execute on Tokio's blocking pool.
401 ///
402 /// # Returns
403 ///
404 /// A [`TaskHandle`] for the accepted blocking task.
405 ///
406 /// # Errors
407 ///
408 /// Returns [`SubmissionError`] if the Tokio blocking domain refuses the
409 /// task.
410 #[inline]
411 pub fn submit_tokio_blocking_callable<C, R, E>(&self, task: C) -> Result<TaskHandle<R, E>, SubmissionError>
412 where
413 C: Callable<R, E> + Send + 'static,
414 R: Send + 'static,
415 E: Send + 'static,
416 {
417 self.tokio_blocking.submit_callable(task)
418 }
419
420 /// Submits a blocking callable task to Tokio and returns a tracked handle.
421 ///
422 /// # Parameters
423 ///
424 /// * `task` - Callable task to execute on Tokio's blocking pool.
425 ///
426 /// # Returns
427 ///
428 /// A [`TokioBlockingTaskHandle`] for the accepted blocking task.
429 ///
430 /// # Errors
431 ///
432 /// Returns [`SubmissionError`] if the Tokio blocking domain refuses the
433 /// task.
434 #[inline]
435 pub fn submit_tracked_tokio_blocking_callable<C, R, E>(
436 &self,
437 task: C,
438 ) -> Result<TokioBlockingTaskHandle<R, E>, SubmissionError>
439 where
440 C: Callable<R, E> + Send + 'static,
441 R: Send + 'static,
442 E: Send + 'static,
443 {
444 self.tokio_blocking.submit_tracked_callable(task)
445 }
446
447 /// Spawns an async IO or Future-based task on Tokio's async runtime.
448 ///
449 /// # Parameters
450 ///
451 /// * `future` - Future to execute on Tokio's async scheduler.
452 ///
453 /// # Returns
454 ///
455 /// A [`TokioTaskHandle`] for the accepted async task.
456 ///
457 /// # Errors
458 ///
459 /// Returns [`SubmissionError`] if the Tokio IO domain refuses the task.
460 #[inline]
461 pub fn spawn_io<F, R, E>(&self, future: F) -> Result<TokioTaskHandle<R, E>, SubmissionError>
462 where
463 F: Future<Output = Result<R, E>> + Send + 'static,
464 R: Send + 'static,
465 E: Send + 'static,
466 {
467 self.io.spawn(future)
468 }
469
470 /// Requests graceful shutdown for every execution domain.
471 pub fn shutdown(&self) {
472 self.blocking.shutdown();
473 self.cpu.shutdown();
474 self.tokio_blocking.shutdown();
475 self.io.shutdown();
476 }
477
478 /// Requests abrupt stop for every execution domain.
479 ///
480 /// # Returns
481 ///
482 /// A per-domain aggregate report describing queued, running, and cancelled
483 /// work observed during shutdown.
484 pub fn stop(&self) -> ExecutionServicesStopReport {
485 ExecutionServicesStopReport {
486 blocking: self.blocking.stop(),
487 cpu: self.cpu.stop(),
488 tokio_blocking: self.tokio_blocking.stop(),
489 io: self.io.stop(),
490 }
491 }
492
493 /// Returns the aggregate lifecycle state.
494 ///
495 /// # Returns
496 ///
497 /// [`ExecutorServiceLifecycle::Terminated`] if all domains have
498 /// terminated; [`ExecutorServiceLifecycle::Stopping`] if any domain is
499 /// stopping; [`ExecutorServiceLifecycle::ShuttingDown`] if any domain is no
500 /// longer running; otherwise [`ExecutorServiceLifecycle::Running`].
501 pub fn lifecycle(&self) -> ExecutorServiceLifecycle {
502 let lifecycles = [
503 self.blocking.lifecycle(),
504 self.cpu.lifecycle(),
505 self.tokio_blocking.lifecycle(),
506 self.io.lifecycle(),
507 ];
508 if lifecycles
509 .iter()
510 .all(|state| *state == ExecutorServiceLifecycle::Terminated)
511 {
512 ExecutorServiceLifecycle::Terminated
513 } else if lifecycles.contains(&ExecutorServiceLifecycle::Stopping) {
514 ExecutorServiceLifecycle::Stopping
515 } else if lifecycles
516 .iter()
517 .any(|state| *state != ExecutorServiceLifecycle::Running)
518 {
519 ExecutorServiceLifecycle::ShuttingDown
520 } else {
521 ExecutorServiceLifecycle::Running
522 }
523 }
524
525 /// Returns whether every execution domain is running.
526 ///
527 /// # Returns
528 ///
529 /// `true` only if all execution domains are running.
530 #[inline]
531 pub fn is_running(&self) -> bool {
532 self.lifecycle() == ExecutorServiceLifecycle::Running
533 }
534
535 /// Returns whether any execution domain is gracefully shutting down.
536 ///
537 /// # Returns
538 ///
539 /// `true` when the aggregate lifecycle is
540 /// [`ExecutorServiceLifecycle::ShuttingDown`].
541 #[inline]
542 pub fn is_shutting_down(&self) -> bool {
543 self.lifecycle() == ExecutorServiceLifecycle::ShuttingDown
544 }
545
546 /// Returns whether any execution domain is stopping abruptly.
547 ///
548 /// # Returns
549 ///
550 /// `true` when the aggregate lifecycle is
551 /// [`ExecutorServiceLifecycle::Stopping`].
552 #[inline]
553 pub fn is_stopping(&self) -> bool {
554 self.lifecycle() == ExecutorServiceLifecycle::Stopping
555 }
556
557 /// Returns whether the facade is no longer fully running.
558 ///
559 /// # Returns
560 ///
561 /// `true` after any execution domain starts shutdown, stop, or has already
562 /// terminated.
563 #[inline]
564 pub fn is_not_running(&self) -> bool {
565 self.lifecycle() != ExecutorServiceLifecycle::Running
566 }
567
568 /// Returns whether every execution domain has terminated.
569 ///
570 /// # Returns
571 ///
572 /// `true` only after all execution domains have terminated.
573 #[inline]
574 pub fn is_terminated(&self) -> bool {
575 self.lifecycle() == ExecutorServiceLifecycle::Terminated
576 }
577
578 /// Waits until every execution domain has terminated.
579 ///
580 /// # Returns
581 ///
582 /// A future that resolves after all execution domains have terminated.
583 pub fn await_termination(&self) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
584 Box::pin(async move {
585 let blocking = Arc::clone(&self.blocking);
586 let cpu = self.cpu.clone();
587 let tokio_blocking = self.tokio_blocking.clone();
588 let blocking_wait = tokio::task::spawn_blocking(move || blocking.wait_termination());
589 let cpu_wait = tokio::task::spawn_blocking(move || cpu.wait_termination());
590 let tokio_blocking_wait = tokio::task::spawn_blocking(move || tokio_blocking.wait_termination());
591 while !self.io.is_terminated() {
592 tokio::time::sleep(Duration::from_millis(10)).await;
593 }
594 let _ = blocking_wait.await;
595 let _ = cpu_wait.await;
596 let _ = tokio_blocking_wait.await;
597 })
598 }
599}