Skip to main content

fedimint_core/
task.rs

1#![cfg_attr(target_family = "wasm", allow(dead_code))]
2
3mod inner;
4
5/// Just-in-time initialization
6pub mod jit;
7pub mod waiter;
8
9use std::future::Future;
10use std::pin::{Pin, pin};
11use std::sync::Arc;
12use std::time::SystemTime;
13
14use fedimint_core::time::now;
15use fedimint_logging::{LOG_TASK, LOG_TEST};
16use futures::future::{self, Either};
17use inner::TaskGroupInner;
18use scopeguard::defer;
19use thiserror::Error;
20use tokio::sync::{oneshot, watch};
21use tracing::{Span, debug, info, trace};
22
23use crate::runtime;
24// TODO: stop using `task::*`, and use `runtime::*` in the code
25// lots of churn though
26pub use crate::runtime::*;
27/// A group of task working together
28///
29/// Using this struct it is possible to spawn one or more
30/// main thread collaborating, which can cooperatively gracefully
31/// shut down, either due to external request, or failure of
32/// one of them.
33///
34/// Each thread should periodically check [`TaskHandle`] or rely
35/// on condition like channel disconnection to detect when it is time
36/// to finish.
37#[derive(Clone, Default, Debug)]
38pub struct TaskGroup {
39    inner: Arc<TaskGroupInner>,
40}
41
42impl TaskGroup {
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    pub fn make_handle(&self) -> TaskHandle {
48        TaskHandle {
49            inner: self.inner.clone(),
50        }
51    }
52
53    /// Create a sub-group
54    ///
55    /// Task subgroup works like an independent [`TaskGroup`], but the parent
56    /// `TaskGroup` will propagate the shut down signal to a sub-group.
57    ///
58    /// In contrast to using the parent group directly, a subgroup allows
59    /// calling [`Self::join_all`] and detecting any panics on just a
60    /// subset of tasks.
61    ///
62    /// The code create a subgroup is responsible for calling
63    /// [`Self::join_all`]. If it won't, the parent subgroup **will not**
64    /// detect any panics in the tasks spawned by the subgroup.
65    pub fn make_subgroup(&self) -> Self {
66        let new_tg = Self::new();
67        self.inner.add_subgroup(new_tg.clone());
68        new_tg
69    }
70
71    /// Is task group shutting down?
72    pub fn is_shutting_down(&self) -> bool {
73        self.inner.is_shutting_down()
74    }
75
76    /// Tell all tasks in the group to shut down. This only initiates the
77    /// shutdown process, it does not wait for the tasks to shut down.
78    pub fn shutdown(&self) {
79        self.inner.shutdown();
80    }
81
82    /// Tell all tasks in the group to shut down and wait for them to finish.
83    pub async fn shutdown_join_all(
84        self,
85        join_timeout: impl Into<Option<Duration>>,
86    ) -> Result<(), JoinAllError> {
87        self.shutdown();
88        self.join_all(join_timeout.into()).await
89    }
90
91    /// Add a task to the group that waits for CTRL+C or SIGTERM, then
92    /// tells the rest of the task group to shut down.
93    #[cfg(not(target_family = "wasm"))]
94    pub fn install_kill_handler(&self) {
95        /// Wait for CTRL+C or SIGTERM.
96        async fn wait_for_shutdown_signal() {
97            use tokio::signal;
98
99            let ctrl_c = async {
100                signal::ctrl_c()
101                    .await
102                    .expect("failed to install Ctrl+C handler");
103            };
104
105            #[cfg(unix)]
106            let terminate = async {
107                signal::unix::signal(signal::unix::SignalKind::terminate())
108                    .expect("failed to install signal handler")
109                    .recv()
110                    .await;
111            };
112
113            #[cfg(not(unix))]
114            let terminate = std::future::pending::<()>();
115
116            tokio::select! {
117                () = ctrl_c => {},
118                () = terminate => {},
119            }
120        }
121
122        runtime::spawn("kill handlers", {
123            let task_group = self.clone();
124            async move {
125                wait_for_shutdown_signal().await;
126                info!(
127                    target: LOG_TASK,
128                    "signal received, starting graceful shutdown"
129                );
130                task_group.shutdown();
131            }
132        });
133    }
134
135    pub fn spawn<Fut, R>(
136        &self,
137        name: impl Into<String>,
138        f: impl FnOnce(TaskHandle) -> Fut + MaybeSend + 'static,
139    ) -> oneshot::Receiver<R>
140    where
141        Fut: Future<Output = R> + MaybeSend + 'static,
142        R: MaybeSend + 'static,
143    {
144        self.spawn_inner(name, f, false, None)
145    }
146
147    /// This is a version of [`Self::spawn`] that uses less noisy logging level
148    ///
149    /// Meant for tasks that are spawned often enough to not be as interesting.
150    pub fn spawn_silent<Fut, R>(
151        &self,
152        name: impl Into<String>,
153        f: impl FnOnce(TaskHandle) -> Fut + MaybeSend + 'static,
154    ) -> oneshot::Receiver<R>
155    where
156        Fut: Future<Output = R> + MaybeSend + 'static,
157        R: MaybeSend + 'static,
158    {
159        self.spawn_inner(name, f, true, None)
160    }
161
162    /// Like [`Self::spawn`] but the spawn span (and the lifecycle events the
163    /// task group emits around the user future) is parented to `parent_span`,
164    /// so events inherit fields from it (e.g. `fed_id`).
165    pub fn spawn_with_span<Fut, R>(
166        &self,
167        parent_span: Span,
168        name: impl Into<String>,
169        f: impl FnOnce(TaskHandle) -> Fut + MaybeSend + 'static,
170    ) -> oneshot::Receiver<R>
171    where
172        Fut: Future<Output = R> + MaybeSend + 'static,
173        R: MaybeSend + 'static,
174    {
175        self.spawn_inner(name, f, false, Some(parent_span))
176    }
177
178    fn spawn_inner<Fut, R>(
179        &self,
180        name: impl Into<String>,
181        f: impl FnOnce(TaskHandle) -> Fut + MaybeSend + 'static,
182        quiet: bool,
183        parent_span: Option<Span>,
184    ) -> oneshot::Receiver<R>
185    where
186        Fut: Future<Output = R> + MaybeSend + 'static,
187        R: MaybeSend + 'static,
188    {
189        let name = name.into();
190        let mut guard = TaskPanicGuard {
191            name: name.clone(),
192            inner: self.inner.clone(),
193            completed: false,
194        };
195        let handle = self.make_handle();
196
197        let (tx, rx) = oneshot::channel();
198        self.inner
199            .active_tasks_join_handles
200            .lock()
201            .expect("Locking failed")
202            .insert_with_key(move |task_key| {
203                let task_future = {
204                    let name = name.clone();
205                    async move {
206                        defer! {
207                            // Panic or normal completion, it means the task
208                            // is complete, and does not need to be shutdown
209                            // via join handle. This prevents buildup of task
210                            // handles.
211                            if handle
212                                .inner
213                                .active_tasks_join_handles
214                                .lock()
215                                .expect("Locking failed")
216                                .remove(task_key)
217                                .is_none() {
218                                    trace!(target: LOG_TASK, %name, "Task already canceled");
219                                }
220                        }
221                        // Unfortunately log levels need to be static
222                        if quiet {
223                            trace!(target: LOG_TASK, %name, "Starting task");
224                        } else {
225                            debug!(target: LOG_TASK, %name, "Starting task");
226                        }
227                        let r = f(handle.clone()).await;
228                        guard.completed = true;
229
230                        if quiet {
231                            trace!(target: LOG_TASK, %name, "Finished task");
232                        } else {
233                            debug!(target: LOG_TASK, %name, "Finished task");
234                        }
235                        // if receiver is not interested, just drop the message
236                        let _ = tx.send(r);
237
238                        // NOTE: Since this is a `async move` the guard will not get moved
239                        // if it's not moved inside the body. Weird.
240                        drop(guard);
241                    }
242                };
243                let join_handle = match parent_span.as_ref() {
244                    Some(parent) => crate::runtime::spawn_with_span(parent, &name, task_future),
245                    None => crate::runtime::spawn(&name, task_future),
246                };
247                (name, join_handle)
248            });
249
250        rx
251    }
252
253    /// Spawn a task that will get cancelled automatically on `TaskGroup`
254    /// shutdown.
255    pub fn spawn_cancellable<R>(
256        &self,
257        name: impl Into<String>,
258        future: impl Future<Output = R> + MaybeSend + 'static,
259    ) -> oneshot::Receiver<Result<R, ShuttingDownError>>
260    where
261        R: MaybeSend + 'static,
262    {
263        self.spawn(name, |handle| async move {
264            let value = handle.cancel_on_shutdown(future).await;
265            if value.is_err() {
266                // name will part of span
267                debug!(target: LOG_TASK, "task cancelled on shutdown");
268            }
269            value
270        })
271    }
272
273    /// Like [`Self::spawn_cancellable`] but with an explicit parent span — see
274    /// [`Self::spawn_with_span`].
275    pub fn spawn_cancellable_with_span<R>(
276        &self,
277        parent_span: Span,
278        name: impl Into<String>,
279        future: impl Future<Output = R> + MaybeSend + 'static,
280    ) -> oneshot::Receiver<Result<R, ShuttingDownError>>
281    where
282        R: MaybeSend + 'static,
283    {
284        self.spawn_with_span(parent_span, name, |handle| async move {
285            let value = handle.cancel_on_shutdown(future).await;
286            if value.is_err() {
287                // name will part of span
288                debug!(target: LOG_TASK, "task cancelled on shutdown");
289            }
290            value
291        })
292    }
293
294    pub fn spawn_cancellable_silent<R>(
295        &self,
296        name: impl Into<String>,
297        future: impl Future<Output = R> + MaybeSend + 'static,
298    ) -> oneshot::Receiver<Result<R, ShuttingDownError>>
299    where
300        R: MaybeSend + 'static,
301    {
302        self.spawn_silent(name, |handle| async move {
303            let value = handle.cancel_on_shutdown(future).await;
304            if value.is_err() {
305                // name will part of span
306                debug!(target: LOG_TASK, "task cancelled on shutdown");
307            }
308            value
309        })
310    }
311
312    pub async fn join_all(self, timeout: Option<Duration>) -> Result<(), JoinAllError> {
313        let deadline = timeout.map(|timeout| now() + timeout);
314        let mut errors = vec![];
315
316        self.join_all_inner(deadline, &mut errors).await;
317
318        if errors.is_empty() {
319            Ok(())
320        } else {
321            Err(JoinAllError { errors })
322        }
323    }
324
325    #[cfg_attr(not(target_family = "wasm"), ::async_recursion::async_recursion)]
326    #[cfg_attr(target_family = "wasm", ::async_recursion::async_recursion(?Send))]
327    pub async fn join_all_inner(self, deadline: Option<SystemTime>, errors: &mut Vec<JoinError>) {
328        self.inner.join_all(deadline, errors).await;
329    }
330}
331
332struct TaskPanicGuard {
333    name: String,
334    inner: Arc<TaskGroupInner>,
335    /// Did the future completed successfully (no panic)
336    completed: bool,
337}
338
339impl Drop for TaskPanicGuard {
340    fn drop(&mut self) {
341        trace!(
342            target: LOG_TASK,
343            name = %self.name,
344            "Task drop"
345        );
346        if !self.completed {
347            info!(
348                target: LOG_TASK,
349                name = %self.name,
350                "Task shut down uncleanly"
351            );
352            self.inner.shutdown();
353        }
354    }
355}
356
357#[derive(Clone, Debug)]
358pub struct TaskHandle {
359    inner: Arc<TaskGroupInner>,
360}
361
362#[derive(thiserror::Error, Debug, Clone)]
363#[error("Task group is shutting down")]
364#[non_exhaustive]
365pub struct ShuttingDownError {}
366
367/// One or more tasks of a [`TaskGroup`] did not finish cleanly.
368#[derive(Debug, Error)]
369#[error("{} tasks did not finish cleanly: {errors:?}", .errors.len())]
370#[non_exhaustive]
371pub struct JoinAllError {
372    /// The join failures collected for tasks that were still registered with
373    /// the group when joining; a task that had already finished or panicked
374    /// before the join is not included.
375    pub errors: Vec<JoinError>,
376}
377
378impl TaskHandle {
379    /// Is task group shutting down?
380    ///
381    /// Every task in a task group should detect and stop if `true`.
382    pub fn is_shutting_down(&self) -> bool {
383        self.inner.is_shutting_down()
384    }
385
386    /// Make a [`oneshot::Receiver`] that will fire on shutdown
387    ///
388    /// Tasks can use `select` on the return value to handle shutdown
389    /// signal during otherwise blocking operation.
390    pub fn make_shutdown_rx(&self) -> TaskShutdownToken {
391        self.inner.make_shutdown_rx()
392    }
393
394    /// Run the future or cancel it if the [`TaskGroup`] shuts down.
395    pub async fn cancel_on_shutdown<F: Future>(
396        &self,
397        fut: F,
398    ) -> Result<F::Output, ShuttingDownError> {
399        let rx = self.make_shutdown_rx();
400        match future::select(pin!(rx), pin!(fut)).await {
401            Either::Left(((), _)) => Err(ShuttingDownError {}),
402            Either::Right((value, _)) => Ok(value),
403        }
404    }
405}
406
407pub struct TaskShutdownToken(Pin<Box<dyn Future<Output = ()> + Send>>);
408
409impl TaskShutdownToken {
410    fn new(mut rx: watch::Receiver<bool>) -> Self {
411        Self(Box::pin(async move {
412            let _ = rx.wait_for(|v| *v).await;
413        }))
414    }
415}
416
417impl Future for TaskShutdownToken {
418    type Output = ();
419
420    fn poll(
421        mut self: Pin<&mut Self>,
422        cx: &mut std::task::Context<'_>,
423    ) -> std::task::Poll<Self::Output> {
424        self.0.as_mut().poll(cx)
425    }
426}
427
428/// async trait that use MaybeSend
429///
430/// # Example
431///
432/// ```rust
433/// use fedimint_core::{apply, async_trait_maybe_send};
434/// #[apply(async_trait_maybe_send!)]
435/// trait Foo {
436///     // methods
437/// }
438///
439/// #[apply(async_trait_maybe_send!)]
440/// impl Foo for () {
441///     // methods
442/// }
443/// ```
444#[macro_export]
445macro_rules! async_trait_maybe_send {
446    ($($tt:tt)*) => {
447        #[cfg_attr(not(target_family = "wasm"), ::async_trait::async_trait)]
448        #[cfg_attr(target_family = "wasm", ::async_trait::async_trait(?Send))]
449        $($tt)*
450    };
451}
452
453/// MaybeSync can not be used in `dyn $Trait + MaybeSend`
454///
455/// # Example
456///
457/// ```rust
458/// use std::any::Any;
459///
460/// use fedimint_core::{apply, maybe_add_send};
461/// type Foo = maybe_add_send!(dyn Any);
462/// ```
463#[cfg(not(target_family = "wasm"))]
464#[macro_export]
465macro_rules! maybe_add_send {
466    ($($tt:tt)*) => {
467        $($tt)* + Send
468    };
469}
470
471/// MaybeSync can not be used in `dyn $Trait + MaybeSend`
472///
473/// # Example
474///
475/// ```rust
476/// type Foo = maybe_add_send!(dyn Any);
477/// ```
478#[cfg(target_family = "wasm")]
479#[macro_export]
480macro_rules! maybe_add_send {
481    ($($tt:tt)*) => {
482        $($tt)*
483    };
484}
485
486/// See `maybe_add_send`
487#[cfg(not(target_family = "wasm"))]
488#[macro_export]
489macro_rules! maybe_add_send_sync {
490    ($($tt:tt)*) => {
491        $($tt)* + Send + Sync
492    };
493}
494
495/// See `maybe_add_send`
496#[cfg(target_family = "wasm")]
497#[macro_export]
498macro_rules! maybe_add_send_sync {
499    ($($tt:tt)*) => {
500        $($tt)*
501    };
502}
503
504/// `MaybeSend` is no-op on wasm and `Send` on non wasm.
505///
506/// On wasm, most types don't implement `Send` because JS types can not sent
507/// between workers directly.
508#[cfg(target_family = "wasm")]
509pub trait MaybeSend {}
510
511/// `MaybeSend` is no-op on wasm and `Send` on non wasm.
512///
513/// On wasm, most types don't implement `Send` because JS types can not sent
514/// between workers directly.
515#[cfg(not(target_family = "wasm"))]
516pub trait MaybeSend: Send {}
517
518#[cfg(not(target_family = "wasm"))]
519impl<T: Send> MaybeSend for T {}
520
521#[cfg(target_family = "wasm")]
522impl<T> MaybeSend for T {}
523
524/// `MaybeSync` is no-op on wasm and `Sync` on non wasm.
525#[cfg(target_family = "wasm")]
526pub trait MaybeSync {}
527
528/// `MaybeSync` is no-op on wasm and `Sync` on non wasm.
529#[cfg(not(target_family = "wasm"))]
530pub trait MaybeSync: Sync {}
531
532#[cfg(not(target_family = "wasm"))]
533impl<T: Sync> MaybeSync for T {}
534
535#[cfg(target_family = "wasm")]
536impl<T> MaybeSync for T {}
537
538// Used in tests when sleep functionality is desired so it can be logged.
539// Must include comment describing the reason for sleeping.
540pub async fn sleep_in_test(comment: impl AsRef<str>, duration: Duration) {
541    info!(
542        target: LOG_TEST,
543        "Sleeping for {}.{:03} seconds because: {}",
544        duration.as_secs(),
545        duration.subsec_millis(),
546        comment.as_ref()
547    );
548    sleep(duration).await;
549}
550
551/// An error used as a "cancelled" marker in [`Cancellable`].
552#[derive(Error, Debug)]
553#[error("Operation cancelled")]
554pub struct Cancelled;
555
556/// Operation that can potentially get cancelled returning no result (e.g.
557/// program shutdown).
558pub type Cancellable<T> = std::result::Result<T, Cancelled>;
559
560#[cfg(test)]
561mod tests;