Skip to main content

stratum_apps/
fallback_coordinator.rs

1use std::future::Future;
2use tokio_util::{sync::CancellationToken, task::TaskTracker};
3
4/// Coordinates fallback across the components of a runtime generation.
5///
6/// Fallback is signalled through a [`CancellationToken`] and acknowledged by lifetime: a future
7/// wrapped with [`FallbackCoordinator::track`] counts as pending until it is dropped, whether it
8/// returned, was cancelled or panicked, so there is nothing a component has to remember to call
9/// on its way out. [`FallbackCoordinator::trigger_fallback_and_wait`] cancels the token and
10/// resolves once no tracked future is left, which it checks rather than infers from a wakeup: a
11/// future tracked after the trigger, or after every other one already finished, is still waited
12/// for.
13///
14/// This is meant to be used together with [`crate::task_manager::TaskManager`]:
15///
16/// ```ignore
17/// task_manager.spawn(fallback_coordinator.track(async move {
18///     tokio::select! {
19///         _ = fallback_token.cancelled() => { /* clean up */ }
20///         // ...
21///     }
22/// }));
23/// ```
24///
25/// A coordinator serves one fallback cycle: once the wait returned, the runtime instantiates a
26/// fresh one for the next generation.
27#[derive(Debug, Clone, Default)]
28pub struct FallbackCoordinator {
29    signal: CancellationToken,
30    tasks: TaskTracker,
31}
32
33impl FallbackCoordinator {
34    pub fn new() -> Self {
35        Self::default()
36    }
37
38    /// Get the cancellation token that signals fallback.
39    pub fn token(&self) -> CancellationToken {
40        self.signal.clone()
41    }
42
43    /// Wrap a fallback-relevant future so that fallback waits for it until it is dropped.
44    ///
45    /// Tracking starts here, not when the future is first polled, so a tracked future handed to a
46    /// spawner is pending from the moment it exists. Track before fallback is triggered or from
47    /// inside a future that is itself tracked: a future tracked after the wait has observed the
48    /// tracker empty belongs to no generation and is not waited for.
49    pub fn track<F: Future>(&self, fut: F) -> impl Future<Output = F::Output> + use<F> {
50        self.tasks.track_future(fut)
51    }
52
53    /// Trigger fallback and wait until every tracked future has been dropped.
54    pub async fn trigger_fallback_and_wait(&self) {
55        tracing::debug!(
56            pending = self.tasks.len(),
57            "FallbackCoordinator: triggering fallback"
58        );
59        self.signal.cancel();
60        self.tasks.close();
61        // `wait` resolves once the tracker has been empty at some point since the call, so work
62        // tracked after that point would not be covered: look again, and wait again if there is
63        // any. `wait` re-reads the closed-and-empty state when it is created and on every poll,
64        // so a drop between the check and the next `wait` is seen by that `wait`, not missed.
65        loop {
66            self.tasks.wait().await;
67            if self.tasks.is_empty() {
68                break;
69            }
70            tracing::debug!(
71                pending = self.tasks.len(),
72                "FallbackCoordinator: work tracked after the tracker emptied, waiting again"
73            );
74        }
75        tracing::debug!("FallbackCoordinator: all components finished fallback cleanup");
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use futures::FutureExt as _;
83    use std::pin::pin;
84
85    #[tokio::test]
86    async fn waits_for_work_tracked_after_an_earlier_one_finished() {
87        let coordinator = FallbackCoordinator::new();
88        let first = coordinator.track(std::future::pending::<()>());
89        let mut wait = pin!(coordinator.trigger_fallback_and_wait());
90        assert!(wait.as_mut().now_or_never().is_none());
91
92        drop(first);
93        let second = coordinator.track(std::future::pending::<()>());
94        assert!(
95            wait.as_mut().now_or_never().is_none(),
96            "fallback completed while a component was still pending"
97        );
98
99        drop(second);
100        assert!(wait.as_mut().now_or_never().is_some());
101    }
102
103    #[tokio::test]
104    async fn tracked_future_counts_from_creation_until_dropped() {
105        let coordinator = FallbackCoordinator::new();
106        let never_polled = coordinator.track(std::future::pending::<()>());
107        let mut wait = pin!(coordinator.trigger_fallback_and_wait());
108        assert!(wait.as_mut().now_or_never().is_none());
109
110        drop(never_polled);
111        assert!(wait.as_mut().now_or_never().is_some());
112    }
113
114    #[tokio::test]
115    async fn panicking_tracked_task_is_released() {
116        let coordinator = FallbackCoordinator::new();
117        let task = tokio::spawn(coordinator.track(async { panic!("task failed") }));
118        assert!(task.await.unwrap_err().is_panic());
119
120        coordinator.trigger_fallback_and_wait().await;
121    }
122}