Skip to main content

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}