Skip to main content

workflow_task/
lib.rs

1//! Abstraction for spawning and managing a re-startable async [`Task`] that runs
2//! a user-supplied async closure, exposing termination and completion channels
3//! for cooperative cancellation and result delivery.
4
5// use workflow_core::task::*;
6use futures::Future;
7use std::marker::PhantomData;
8use std::pin::Pin;
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::sync::{Arc, Mutex};
11use thiserror::Error;
12use workflow_core::channel::{
13    Receiver, RecvError, SendError, Sender, TryRecvError, TrySendError, oneshot,
14};
15pub use workflow_task_macros::{set_task, task};
16
17/// Errors produced by the [`Task`] implementation
18#[derive(Debug, Error)]
19pub enum TaskError {
20    /// The task is not currently running.
21    #[error("The task is not running")]
22    NotRunning,
23    /// The task is already running and cannot be started again.
24    #[error("The task is already running")]
25    AlreadyRunning,
26    /// Failed to send on one of the task's channels.
27    #[error("Task channel send error {0}")]
28    SendError(String),
29    /// Failed to receive on one of the task's channels.
30    #[error("Task channel receive error: {0:?}")]
31    RecvError(#[from] RecvError),
32    /// Non-blocking send on a task channel failed.
33    #[error("Task channel try send error: {0}")]
34    TrySendError(String),
35    /// Non-blocking receive on a task channel failed.
36    #[error("Task channel try receive {0:?}")]
37    TryRecvError(#[from] TryRecvError),
38}
39
40impl<T> From<SendError<T>> for TaskError {
41    fn from(err: SendError<T>) -> Self {
42        TaskError::SendError(err.to_string())
43    }
44}
45
46impl<T> From<TrySendError<T>> for TaskError {
47    fn from(err: TrySendError<T>) -> Self {
48        TaskError::SendError(err.to_string())
49    }
50}
51
52/// Result type used by the [`Task`] implementation
53pub type TaskResult<T> = std::result::Result<T, TaskError>;
54
55/// Boxed async closure executed by a [`Task`], receiving the task argument and a
56/// termination [`Receiver`] and returning the task's future via [`FnReturn`].
57pub type TaskFn<A, T> = Arc<Box<dyn Send + Sync + Fn(A, Receiver<()>) -> FnReturn<T> + 'static>>;
58/// Pinned, boxed future returned by a [`TaskFn`], resolving to the task's output value.
59pub type FnReturn<T> = Pin<Box<dyn Send + Sync + 'static + Future<Output = T>>>;
60
61struct TaskInner<A, T>
62where
63    A: Send,
64    T: 'static,
65{
66    termination: (Sender<()>, Receiver<()>),
67    completion: (Sender<T>, Receiver<T>),
68    running: Arc<AtomicBool>,
69    task_fn: Arc<Mutex<Option<TaskFn<A, T>>>>,
70    args: PhantomData<A>,
71}
72
73impl<A, T> TaskInner<A, T>
74where
75    A: Send + Sync + 'static,
76    T: Send + 'static,
77{
78    fn new_with_boxed_task_fn<FN>(task_fn: Box<FN>) -> Self
79    //TaskInner<A, T>
80    where
81        FN: Send + Sync + Fn(A, Receiver<()>) -> FnReturn<T> + 'static,
82    {
83        let termination = oneshot();
84        let completion = oneshot();
85
86        TaskInner {
87            termination,
88            completion,
89            running: Arc::new(AtomicBool::new(false)),
90            task_fn: Arc::new(Mutex::new(Some(Arc::new(task_fn)))),
91            args: PhantomData,
92        }
93    }
94
95    pub fn blank() -> Self {
96        let termination = oneshot();
97        let completion = oneshot();
98        TaskInner {
99            termination,
100            completion,
101            running: Arc::new(AtomicBool::new(false)),
102            task_fn: Arc::new(Mutex::new(None)),
103            args: PhantomData,
104        }
105    }
106
107    fn task_fn(&self) -> TaskFn<A, T> {
108        self.task_fn
109            .lock()
110            .unwrap()
111            .as_ref()
112            .expect("Task::task_fn is not initialized")
113            .clone()
114    }
115
116    /// Replace task fn with an alternate function.
117    /// The passed function must be boxed.
118    fn set_boxed_task_fn(
119        &self,
120        task_fn: Box<dyn Send + Sync + Fn(A, Receiver<()>) -> FnReturn<T> + 'static>,
121    ) {
122        let task_fn = Arc::new(task_fn);
123        *self.task_fn.lock().unwrap() = Some(task_fn);
124    }
125
126    pub fn run(self: &Arc<Self>, args: A) -> TaskResult<&Arc<Self>> {
127        if !self.completion.1.is_empty() {
128            panic!("Task::run(): task completion channel is not empty");
129        }
130
131        if !self.termination.1.is_empty() {
132            panic!("Task::run(): task termination channel is not empty");
133        }
134
135        let this = self.clone();
136        let cb = self.task_fn();
137        workflow_core::task::spawn(async move {
138            this.running.store(true, Ordering::SeqCst);
139
140            let result = cb(args, this.termination.1.clone()).await;
141            this.running.store(false, Ordering::SeqCst);
142            this.completion
143                .0
144                .send(result)
145                .await
146                .expect("Error signaling task completion");
147        });
148
149        Ok(self)
150    }
151
152    pub fn stop(&self) -> TaskResult<()> {
153        if self.running.load(Ordering::SeqCst) {
154            self.termination.0.try_send(())?;
155        }
156        Ok(())
157    }
158
159    /// Blocks until the task exits. Resolves immediately
160    /// if the task is not running.
161    pub async fn join(&self) -> TaskResult<T> {
162        if self.running.load(Ordering::SeqCst) {
163            Ok(self.completion.1.recv().await?)
164        } else {
165            Err(TaskError::NotRunning)
166        }
167    }
168
169    /// Signals termination and blocks until the
170    /// task exits.
171    pub async fn stop_and_join(&self) -> TaskResult<T> {
172        if self.running.load(Ordering::SeqCst) {
173            self.termination.0.send(()).await?;
174            Ok(self.completion.1.recv().await?)
175        } else {
176            Err(TaskError::NotRunning)
177        }
178    }
179
180    pub fn is_running(&self) -> bool {
181        self.running.load(Ordering::SeqCst)
182    }
183}
184
185/// [`Task`]{self::Task} struct allows you to spawn an async fn that can run
186/// in a loop as a task (similar to a thread), checking for a
187/// termination signal (so that execution can be aborted),
188/// upon completion returning a value to the creator.
189///
190/// You can pass a [`channel`](workflow_core::channel::Receiver) as an argument to the async
191/// function if you wish to communicate with the task.
192///
193/// NOTE: You should always call `task.join().await` to await
194/// for the task completion if re-using the task.
195///
196/// ```rust
197/// use workflow_task::{task, TaskResult};
198///
199/// # #[tokio::test]
200/// # async fn test()->TaskResult<()>{
201///
202/// let task = task!(
203///     |args : (), stop : Receiver<()>| async move {
204///         let mut index = args;
205///         loop {
206///             if stop.try_recv().is_ok() {
207///                 break;
208///             }
209///             // ... do something ...
210///             index += 1;
211///         }
212///         return index;
213///     }
214/// );
215///
216/// // spawn the task instance ...
217/// // passing 256 as the `args` argument
218/// task.run(256)?;
219///
220/// // signal termination ...
221/// task.stop()?;
222///
223/// // await for the task completion ...
224/// // the `result` is the returned `index` value
225/// let result = task.join().await?;
226///
227/// // rinse and repeat if needed
228/// task.run(256)?;
229///
230/// # Ok(())
231/// # }
232///
233/// ```
234///
235#[derive(Clone)]
236pub struct Task<A, T>
237where
238    A: Send,
239    T: 'static,
240{
241    inner: Arc<TaskInner<A, T>>,
242}
243
244impl<A, T> Default for Task<A, T>
245where
246    A: Send + Sync + 'static,
247    T: Send + Sync + 'static,
248{
249    fn default() -> Self {
250        Task::blank()
251    }
252}
253
254impl<A, T> Task<A, T>
255where
256    A: Send + Sync + 'static,
257    T: Send + 'static,
258{
259    ///
260    /// Create a new [`Task`](self::Task) instance by supplying it with
261    /// an async closure that has 2 arguments:
262    /// ```rust
263    /// use workflow_task::task;
264    ///
265    /// task!(|args:bool, signal| async move {
266    ///     // ...
267    ///     return true;
268    /// });
269    /// ```
270    pub fn new<FN>(task_fn: FN) -> Task<A, T>
271    where
272        FN: Send + Sync + Fn(A, Receiver<()>) -> FnReturn<T> + 'static,
273    {
274        Self::new_with_boxed_task_fn(Box::new(task_fn))
275    }
276
277    fn new_with_boxed_task_fn<FN>(task_fn: Box<FN>) -> Task<A, T>
278    where
279        FN: Send + Sync + Fn(A, Receiver<()>) -> FnReturn<T> + 'static,
280    {
281        Task {
282            inner: Arc::new(TaskInner::new_with_boxed_task_fn(task_fn)),
283        }
284    }
285
286    /// Create an instance of the task without any task function.
287    /// The task function can be passed later via [`Task::set_task_fn()`].
288    pub fn blank() -> Self {
289        Task {
290            inner: Arc::new(TaskInner::blank()),
291        }
292    }
293
294    /// Replace task fn with an alternate function.
295    /// The task must be restarted for the replacement
296    /// to take effect.  The function passed does not
297    /// need to be boxed.
298    pub fn set_task_fn<FN>(&self, task_fn: FN)
299    where
300        FN: Send + Sync + Fn(A, Receiver<()>) -> FnReturn<T> + 'static,
301    {
302        self.inner.set_boxed_task_fn(Box::new(task_fn))
303    }
304
305    /// Run the task supplying the provided argument to the
306    /// closure supplied at creation.
307    pub fn run(&self, args: A) -> TaskResult<&Self> {
308        self.inner.run(args)?;
309        Ok(self)
310    }
311
312    /// Signal termination on the channel supplied
313    /// to the task closure; The task has to check
314    /// for the signal periodically or await on
315    /// the future of the signal.
316    pub fn stop(&self) -> TaskResult<()> {
317        self.inner.stop()
318    }
319
320    /// Blocks until the task exits. Resolves immediately
321    /// if the task is not running.
322    pub async fn join(&self) -> TaskResult<T> {
323        self.inner.join().await
324    }
325
326    /// Signals termination and blocks until the
327    /// task exits.
328    pub async fn stop_and_join(&self) -> TaskResult<T> {
329        self.inner.stop_and_join().await
330    }
331
332    /// Returns `true` if the task is running, otherwise
333    /// returns `false`.
334    pub fn is_running(&self) -> bool {
335        self.inner.is_running()
336    }
337}
338
339#[cfg(not(target_arch = "wasm32"))]
340#[cfg(test)]
341mod test {
342
343    use super::*;
344    use std::time::Duration;
345
346    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
347    pub async fn test_task() {
348        let task = Task::new(|args, stop| -> FnReturn<String> {
349            Box::pin(async move {
350                println!("starting task... {}", args);
351                for i in 0..10 {
352                    if stop.try_recv().is_ok() {
353                        println!("stopping task...");
354                        break;
355                    }
356                    println!("t: {}", i);
357                    workflow_core::task::sleep(Duration::from_millis(500)).await;
358                }
359                println!("exiting task...");
360                format!("finished {args}")
361            })
362        });
363
364        task.run("- first -").ok();
365
366        for i in 0..5 {
367            println!("m: {}", i);
368            workflow_core::task::sleep(Duration::from_millis(500)).await;
369        }
370
371        let ret1 = task.join().await.expect("[ret1] task wait failed");
372        println!("ret1: {:?}", ret1);
373
374        task.stop().ok();
375
376        task.run("- second -").ok();
377
378        for i in 0..5 {
379            println!("m: {}", i);
380            workflow_core::task::sleep(Duration::from_millis(500)).await;
381        }
382
383        task.stop().ok();
384        let ret2 = task.join().await.expect("[ret2] task wait failed");
385        println!("ret2: {:?}", ret2);
386
387        println!("done");
388    }
389}