Skip to main content

qubit_thread_pool/
pool_job.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    panic::{
12        AssertUnwindSafe,
13        catch_unwind,
14    },
15    sync::Mutex,
16};
17
18use qubit_executor::task::spi::{
19    TaskRunner,
20    TaskSlot,
21};
22use qubit_function::{
23    Callable,
24    Runnable,
25};
26
27/// Type-erased callable owned by a pool queue.
28trait PoolTask: Send + 'static {
29    /// Marks this task as accepted by an executor service.
30    ///
31    /// # Returns
32    ///
33    /// `true` when the acceptance callback completed, or `false` when a custom
34    /// callback panicked and was contained.
35    fn accept(&self) -> bool;
36
37    /// Runs this task and publishes its result if it was not cancelled first.
38    fn run(self: Box<Self>);
39
40    /// Cancels this task before it starts.
41    fn cancel(self: Box<Self>);
42}
43
44/// Callable task paired with its runner-side completion endpoint.
45struct CompletablePoolTask<C, R, E> {
46    /// Callable task to execute once a worker starts this job.
47    task: C,
48    /// Completion endpoint used to publish the task result.
49    completion: TaskSlot<R, E>,
50}
51
52impl<C, R, E> PoolTask for CompletablePoolTask<C, R, E>
53where
54    C: Callable<R, E> + Send + 'static,
55    R: Send + 'static,
56    E: Send + 'static,
57{
58    /// Marks this task as accepted by an executor service.
59    fn accept(&self) -> bool {
60        self.completion.accept();
61        true
62    }
63
64    /// Runs this task and publishes its result if it was not cancelled first.
65    fn run(self: Box<Self>) {
66        let Self { task, completion } = *self;
67        TaskRunner::new(task).run(completion);
68    }
69
70    /// Publishes cancellation for an unstarted accepted task.
71    fn cancel(self: Box<Self>) {
72        let Self { completion, .. } = *self;
73        let _cancelled = completion.cancel_unstarted();
74    }
75}
76
77/// Custom job callbacks supplied by higher-level services.
78///
79/// The callbacks are executed synchronously by the pool path that reaches the
80/// corresponding lifecycle event, so they must stay short and non-blocking.
81/// Panics from custom callbacks are contained before they can escape into pool
82/// worker or shutdown accounting.
83struct CustomPoolTask {
84    /// Callback invoked once the pool accepts the job.
85    accept: Mutex<Option<Box<dyn FnOnce() + Send + 'static>>>,
86    /// Callback executed once a worker starts this job.
87    run: Box<dyn FnOnce() + Send + 'static>,
88    /// Callback executed if the job is cancelled before it starts.
89    cancel: Box<dyn FnOnce() + Send + 'static>,
90}
91
92impl PoolTask for CustomPoolTask {
93    /// Runs the acceptance callback once.
94    fn accept(&self) -> bool {
95        if let Some(accept) = self
96            .accept
97            .lock()
98            .unwrap_or_else(std::sync::PoisonError::into_inner)
99            .take()
100        {
101            return catch_unwind(AssertUnwindSafe(accept)).is_ok();
102        }
103        true
104    }
105
106    /// Runs this custom job.
107    fn run(self: Box<Self>) {
108        let Self { run, .. } = *self;
109        let _ignored = catch_unwind(AssertUnwindSafe(run));
110    }
111
112    /// Cancels this custom job before it starts.
113    fn cancel(self: Box<Self>) {
114        let Self { cancel, .. } = *self;
115        let _ignored = catch_unwind(AssertUnwindSafe(cancel));
116    }
117}
118
119/// Type-erased pool job with separate detached and cancellable forms.
120pub struct PoolJob {
121    /// Internal job representation hidden behind method-only access.
122    inner: PoolJobInner,
123}
124
125/// Private type-erased pool job representation.
126enum PoolJobInner {
127    /// Fire-and-forget job submitted without a completion endpoint.
128    Detached {
129        /// Callback executed once a worker starts the job.
130        run: Box<dyn FnOnce() + Send + 'static>,
131    },
132    /// Job whose queued cancellation must complete a result endpoint.
133    Completable(Box<dyn PoolTask>),
134}
135
136impl PoolJob {
137    /// Creates a custom cancellable job with no acceptance callback.
138    ///
139    /// Higher-level services that maintain their own task state usually want
140    /// [`Self::with_accept`] instead, so they can publish acceptance only after
141    /// the backing pool has accepted the job.
142    /// Custom callbacks run synchronously and should not block. Panics raised
143    /// by `run` or `cancel` are caught and ignored by the pool job wrapper.
144    ///
145    /// # Parameters
146    ///
147    /// * `run` - Callback executed when a worker starts this job.
148    /// * `cancel` - Callback executed if the accepted job is cancelled before
149    ///   it starts.
150    ///
151    /// # Returns
152    ///
153    /// A custom type-erased job accepted by thread pools.
154    pub fn new(run: Box<dyn FnOnce() + Send + 'static>, cancel: Box<dyn FnOnce() + Send + 'static>) -> Self {
155        Self::with_accept(Box::new(|| {}), run, cancel)
156    }
157
158    /// Creates a custom cancellable job with an acceptance callback.
159    ///
160    /// The pool invokes `accept` exactly once after the submission crosses the
161    /// acceptance boundary. If submission is rejected before acceptance, neither
162    /// `accept`, `run`, nor `cancel` is invoked. Custom callbacks run
163    /// synchronously and should not block. Panics raised by these callbacks are
164    /// caught and ignored by the pool job wrapper; an `accept` panic is reported
165    /// to the pool as a failed acceptance callback.
166    ///
167    /// # Parameters
168    ///
169    /// * `accept` - Callback invoked once the pool accepts the job.
170    /// * `run` - Callback executed when a worker starts this job.
171    /// * `cancel` - Callback executed if the accepted job is cancelled before
172    ///   it starts.
173    ///
174    /// # Returns
175    ///
176    /// A custom type-erased job accepted by thread pools.
177    pub fn with_accept(
178        accept: Box<dyn FnOnce() + Send + 'static>,
179        run: Box<dyn FnOnce() + Send + 'static>,
180        cancel: Box<dyn FnOnce() + Send + 'static>,
181    ) -> Self {
182        Self {
183            inner: PoolJobInner::Completable(Box::new(CustomPoolTask {
184                accept: Mutex::new(Some(accept)),
185                run,
186                cancel,
187            })),
188        }
189    }
190
191    /// Creates a pool job from a typed callable task and completion endpoint.
192    ///
193    /// # Parameters
194    ///
195    /// * `task` - Callable task to execute when a worker starts this job.
196    /// * `completion` - Completion endpoint used to publish the typed result or
197    ///   cancellation.
198    ///
199    /// # Returns
200    ///
201    /// A type-erased job that runs the task on worker start and cancels the
202    /// completion endpoint if the job is cancelled while queued.
203    pub(crate) fn from_task<C, R, E>(task: C, completion: TaskSlot<R, E>) -> Self
204    where
205        C: Callable<R, E> + Send + 'static,
206        R: Send + 'static,
207        E: Send + 'static,
208    {
209        Self {
210            inner: PoolJobInner::Completable(Box::new(CompletablePoolTask { task, completion })),
211        }
212    }
213
214    /// Creates a pool job from a runnable task without retaining a result handle.
215    ///
216    /// # Parameters
217    ///
218    /// * `task` - Runnable task to execute when a worker starts this job.
219    ///
220    /// # Returns
221    ///
222    /// A type-erased job that runs the task and discards its final result. If
223    /// the job is abandoned while queued, cancellation has no result endpoint to
224    /// notify.
225    pub(crate) fn detached<T, E>(task: T) -> Self
226    where
227        T: Runnable<E> + Send + 'static,
228        E: Send + 'static,
229    {
230        Self {
231            inner: PoolJobInner::Detached {
232                run: Box::new(move || {
233                    let mut task = task;
234                    let _ignored = catch_unwind(AssertUnwindSafe(|| task.run()));
235                }),
236            },
237        }
238    }
239
240    /// Marks this job as accepted by an executor service.
241    ///
242    /// Detached jobs do not have a completion endpoint, so this is a no-op for
243    /// fire-and-forget submissions.
244    ///
245    /// # Returns
246    ///
247    /// `true` when the job can continue to execution or queueing, or `false`
248    /// when a custom acceptance callback panicked and was contained.
249    pub(crate) fn accept(&self) -> bool {
250        if let PoolJobInner::Completable(task) = &self.inner {
251            return task.accept();
252        }
253        true
254    }
255
256    /// Runs this job if it has not been cancelled first.
257    ///
258    /// Consumes the job and invokes the run callback at most once.
259    pub(crate) fn run(self) {
260        match self.inner {
261            PoolJobInner::Detached { run } => run(),
262            PoolJobInner::Completable(task) => task.run(),
263        }
264    }
265
266    /// Cancels this queued job if it has not been run first.
267    ///
268    /// Consumes the job and invokes the cancellation callback at most once.
269    pub(crate) fn cancel(self) {
270        if let PoolJobInner::Completable(task) = self.inner {
271            task.cancel();
272        }
273    }
274}