Skip to main content

qubit_rayon_executor/
rayon_executor_service.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::sync::{
11    Arc,
12    Mutex,
13};
14
15use qubit_function::{
16    Callable,
17    Runnable,
18};
19use rayon::ThreadPool as RayonThreadPool;
20
21use qubit_executor::{
22    TaskHandle,
23    task::spi::{
24        TaskEndpointPair,
25        TaskRunner,
26        TaskSlot,
27    },
28};
29
30use qubit_executor::service::{
31    ExecutorService,
32    ExecutorServiceLifecycle,
33    StopReport,
34    SubmissionError,
35};
36
37use crate::{
38    pending_cancel::PendingCancel,
39    rayon_executor_service_build_error::RayonExecutorServiceBuildError,
40    rayon_executor_service_builder::RayonExecutorServiceBuilder,
41    rayon_executor_service_state::RayonExecutorServiceState,
42    rayon_task_handle::RayonTaskHandle,
43};
44
45/// Rayon-backed executor service for CPU-bound synchronous tasks.
46///
47/// Accepted tasks are executed on a dedicated Rayon thread pool. The service
48/// preserves the crate's `ExecutorService` lifecycle semantics and task-handle
49/// APIs while delegating scheduling to Rayon.
50#[derive(Clone)]
51pub struct RayonExecutorService {
52    /// Rayon thread pool used to execute accepted tasks.
53    pub(crate) pool: Arc<RayonThreadPool>,
54    /// Shared lifecycle and cancellation state.
55    pub(crate) state: Arc<RayonExecutorServiceState>,
56}
57
58impl RayonExecutorService {
59    /// Creates a Rayon executor service with default builder settings.
60    ///
61    /// # Returns
62    ///
63    /// `Ok(RayonExecutorService)` if the default Rayon thread pool can be
64    /// built.
65    ///
66    /// # Errors
67    ///
68    /// Returns [`RayonExecutorServiceBuildError`] if the default builder
69    /// configuration is rejected.
70    #[inline]
71    pub fn new() -> Result<Self, RayonExecutorServiceBuildError> {
72        Self::builder().build()
73    }
74
75    /// Creates a builder for configuring a Rayon executor service.
76    ///
77    /// # Returns
78    ///
79    /// A builder configured with CPU-parallelism defaults.
80    #[inline]
81    pub fn builder() -> RayonExecutorServiceBuilder {
82        RayonExecutorServiceBuilder::default()
83    }
84
85    /// Accepts a callable, schedules it on the Rayon pool, and returns its handle data.
86    ///
87    /// # Parameters
88    ///
89    /// * `task` - Callable to execute on a Rayon worker.
90    /// * `split` - Function that splits a task endpoint pair into the caller
91    ///   handle and runner slot required by the chosen handle type.
92    ///
93    /// # Returns
94    ///
95    /// The caller-facing handle, stable task identifier, and pending
96    /// cancellation hook.
97    ///
98    /// # Errors
99    ///
100    /// Returns [`SubmissionError::Shutdown`] if shutdown or stop has already
101    /// been requested before the task is accepted.
102    fn submit_callable_with<C, R, E, H, F>(
103        &self,
104        task: C,
105        split: F,
106    ) -> Result<(H, usize, PendingCancel), SubmissionError>
107    where
108        C: Callable<R, E> + Send + 'static,
109        R: Send + 'static,
110        E: Send + 'static,
111        F: FnOnce(TaskEndpointPair<R, E>) -> (H, TaskSlot<R, E>),
112    {
113        let submission_guard = self.state.lock_submission();
114        if self.state.is_not_running() {
115            return Err(SubmissionError::Shutdown);
116        }
117        let task_id = self.state.next_task_id();
118        self.state.on_task_accepted();
119        let (handle, completion) = split(TaskEndpointPair::new());
120        completion.accept();
121        let completion = Arc::new(Mutex::new(Some(completion)));
122        let completion_for_cancel = Arc::clone(&completion);
123        let cancel: PendingCancel = Arc::new(move || {
124            let completion = completion_for_cancel
125                .lock()
126                .unwrap_or_else(std::sync::PoisonError::into_inner)
127                .take();
128            completion.is_some_and(|completion| completion.cancel_unstarted())
129        });
130        self.state.register_pending_task(task_id, Arc::clone(&cancel));
131        drop(submission_guard);
132
133        let completion_for_run = completion;
134        let state_for_run = Arc::clone(&self.state);
135        self.pool.spawn_fifo(move || {
136            let mut running_completion = None;
137            if !state_for_run.start_pending_task(task_id, || {
138                let mut completion = completion_for_run
139                    .lock()
140                    .unwrap_or_else(std::sync::PoisonError::into_inner);
141                let Some(task_completion) = completion.take() else {
142                    return false;
143                };
144                match task_completion.try_start() {
145                    Ok(running) => {
146                        running_completion = Some(running);
147                        true
148                    }
149                    Err(task_completion) => {
150                        *completion = Some(task_completion);
151                        false
152                    }
153                }
154            }) {
155                return;
156            }
157            let running_completion = running_completion.expect("claimed pending task should own a running slot");
158            TaskRunner::new(task).run_started(running_completion);
159            state_for_run.on_task_completed();
160        });
161        Ok((handle, task_id, cancel))
162    }
163}
164
165impl ExecutorService for RayonExecutorService {
166    type ResultHandle<R, E>
167        = TaskHandle<R, E>
168    where
169        R: Send + 'static,
170        E: Send + 'static;
171
172    type TrackedHandle<R, E>
173        = RayonTaskHandle<R, E>
174    where
175        R: Send + 'static,
176        E: Send + 'static;
177
178    /// Accepts a runnable and schedules it on the Rayon thread pool.
179    fn submit<T, E>(&self, task: T) -> Result<(), SubmissionError>
180    where
181        T: Runnable<E> + Send + 'static,
182        E: Send + 'static,
183    {
184        let submission_guard = self.state.lock_submission();
185        if self.state.is_not_running() {
186            return Err(SubmissionError::Shutdown);
187        }
188        let task_id = self.state.next_task_id();
189        self.state.on_task_accepted();
190        let cancel: PendingCancel = Arc::new(|| true);
191        self.state.register_pending_task(task_id, Arc::clone(&cancel));
192        drop(submission_guard);
193
194        let state_for_run = Arc::clone(&self.state);
195        self.pool.spawn_fifo(move || {
196            if !state_for_run.start_pending_task(task_id, || true) {
197                return;
198            }
199            let mut task = task;
200            let _ignored = TaskRunner::new(move || task.run()).call::<(), E>();
201            state_for_run.on_task_completed();
202        });
203        Ok(())
204    }
205
206    /// Accepts a callable and schedules it on the Rayon thread pool.
207    ///
208    /// # Parameters
209    ///
210    /// * `task` - Callable to execute on a Rayon worker.
211    ///
212    /// # Returns
213    ///
214    /// A [`TaskHandle`] for the accepted task.
215    ///
216    /// # Errors
217    ///
218    /// Returns [`SubmissionError::Shutdown`] if shutdown has already been
219    /// requested before the task is accepted.
220    fn submit_callable<C, R, E>(&self, task: C) -> Result<Self::ResultHandle<R, E>, SubmissionError>
221    where
222        C: Callable<R, E> + Send + 'static,
223        R: Send + 'static,
224        E: Send + 'static,
225    {
226        let (handle, _, _) = self.submit_callable_with(task, TaskEndpointPair::into_parts)?;
227        Ok(handle)
228    }
229
230    /// Accepts a callable and schedules it with a tracked handle.
231    fn submit_tracked_callable<C, R, E>(&self, task: C) -> Result<Self::TrackedHandle<R, E>, SubmissionError>
232    where
233        C: Callable<R, E> + Send + 'static,
234        R: Send + 'static,
235        E: Send + 'static,
236    {
237        let (handle, task_id, cancel) = self.submit_callable_with(task, TaskEndpointPair::into_tracked_parts)?;
238        Ok(RayonTaskHandle::new(handle, task_id, Arc::clone(&self.state), cancel))
239    }
240
241    /// Stops accepting new tasks.
242    ///
243    /// Already accepted Rayon tasks are allowed to finish normally.
244    fn shutdown(&self) {
245        let _guard = self.state.lock_submission();
246        self.state.shutdown();
247        self.state.notify_if_terminated();
248    }
249
250    /// Stops accepting new tasks and cancels tasks that have not started yet.
251    ///
252    /// Running Rayon tasks cannot be preempted. Cancellation therefore applies
253    /// only to tasks that are still pending when the cancellation hook wins the
254    /// race against task start.
255    ///
256    /// # Returns
257    ///
258    /// A count-based report describing the pending and running work observed at
259    /// the time of the stop request, plus the number of pending tasks for
260    /// which cancellation succeeded.
261    fn stop(&self) -> StopReport {
262        let _guard = self.state.lock_submission();
263        self.state.stop();
264        self.state.cancel_pending_tasks_for_stop()
265    }
266
267    /// Returns the current lifecycle state.
268    fn lifecycle(&self) -> ExecutorServiceLifecycle {
269        self.state.lifecycle()
270    }
271
272    /// Returns whether shutdown has been requested.
273    fn is_not_running(&self) -> bool {
274        self.state.is_not_running()
275    }
276
277    /// Returns whether shutdown was requested and no accepted tasks remain.
278    fn is_terminated(&self) -> bool {
279        self.lifecycle() == ExecutorServiceLifecycle::Terminated
280    }
281
282    /// Blocks until the service has terminated.
283    fn wait_termination(&self) {
284        self.state.wait_for_termination();
285    }
286}