Skip to main content

rx_rust/scheduler/
mod.rs

1#[cfg(all(feature = "async-std-scheduler", not(feature = "single-threaded")))]
2pub mod async_std_scheduler;
3#[cfg(feature = "local-pool-scheduler")]
4pub mod local_pool_scheduler;
5#[cfg(all(feature = "smol-scheduler", not(feature = "single-threaded")))]
6pub mod smol_scheduler;
7#[cfg(all(feature = "thread-pool-scheduler", not(feature = "single-threaded")))]
8pub mod thread_pool_scheduler;
9#[cfg(all(feature = "tokio-scheduler", not(feature = "single-threaded")))]
10pub mod tokio_scheduler;
11
12use crate::{
13    disposable::{Disposable, bound_drop_disposal::BoundDropDisposal},
14    utils::types::MaybeSend,
15};
16use educe::Educe;
17#[cfg(feature = "futures")]
18use futures::{Stream, stream::StreamExt};
19use std::{
20    pin::Pin,
21    task::{Context, Poll},
22    time::{Duration, Instant},
23};
24
25/// Indicates how a recursive scheduling step should continue.
26#[derive(Educe)]
27#[educe(Debug, Clone, PartialEq, Eq)]
28pub enum RecursionAction {
29    ContinueAt(Instant),
30    ContinueImmediately,
31    Stop,
32}
33
34/// A future that yields to the executor exactly once before completing.
35///
36/// Runtime-agnostic replacement for `yield_now`: it guarantees an await point
37/// so other tasks can make progress and disposal/abort can take effect.
38/// Note that `sleep(Duration::ZERO)` is NOT a substitute — e.g. tokio's
39/// `sleep` with an already-elapsed deadline completes on the first poll
40/// without ever yielding.
41struct YieldNow(bool);
42
43impl Future for YieldNow {
44    type Output = ();
45
46    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
47        if self.0 {
48            Poll::Ready(())
49        } else {
50            self.0 = true;
51            cx.waker().wake_by_ref();
52            Poll::Pending
53        }
54    }
55}
56
57/// Core abstraction for driving asynchronous work across runtimes.
58/// See <https://reactivex.io/documentation/scheduler.html>
59/// This is why the task must be 'static: <https://stackoverflow.com/a/65287449/9315497>
60pub trait Scheduler {
61    type D: Disposable + MaybeSend + 'static;
62
63    fn spawn_future(
64        &self,
65        future: impl Future<Output = ()> + MaybeSend + 'static,
66    ) -> BoundDropDisposal<Self::D>;
67
68    /// Returns a future that completes `duration` after this call.
69    ///
70    /// Contract for implementors: the deadline is captured when `sleep` is
71    /// *called*, not when the returned future is first polled.
72    fn sleep(&self, duration: Duration) -> impl Future + MaybeSend + 'static + use<Self>;
73
74    fn schedule(
75        &self,
76        task: impl FnOnce() + MaybeSend + 'static,
77        delay: Option<Duration>,
78    ) -> BoundDropDisposal<Self::D> {
79        let delay = delay.map(|duration| self.sleep(duration));
80        self.spawn_future(async move {
81            if let Some(delay) = delay {
82                delay.await;
83            }
84            task()
85        })
86    }
87
88    /// Repeatedly runs `task` until it returns [`RecursionAction::Stop`].
89    ///
90    /// The loop yields to the executor between iterations (even for
91    /// `ContinueImmediately` and already-elapsed `ContinueAt` instants), so
92    /// other tasks can make progress and disposal can take effect.
93    fn schedule_recursively(
94        &self,
95        mut task: impl FnMut(usize) -> RecursionAction + MaybeSend + 'static,
96        delay: Option<Duration>,
97    ) -> BoundDropDisposal<Self::D>
98    where
99        Self: Clone + MaybeSend + 'static,
100    {
101        let delay = delay.map(|duration| self.sleep(duration));
102        let self_cloned = self.clone();
103        self.spawn_future(async move {
104            if let Some(delay) = delay {
105                delay.await;
106            }
107            let mut count = 0;
108            loop {
109                match task(count) {
110                    RecursionAction::ContinueAt(at) => {
111                        if let Some(delay) = at.checked_duration_since(Instant::now()) {
112                            self_cloned.sleep(delay).await;
113                        } else {
114                            // The requested instant has already passed;
115                            // still yield so the loop stays cancellable.
116                            YieldNow(false).await;
117                        }
118                    }
119                    RecursionAction::ContinueImmediately => {
120                        // Yield so other tasks can run and disposal can take effect.
121                        YieldNow(false).await;
122                    }
123                    RecursionAction::Stop => break,
124                }
125                count += 1;
126            }
127        })
128    }
129
130    /// Runs `task` at a fixed rate anchored to the time of this call
131    /// (plus `delay`), until `task` returns `false`.
132    ///
133    /// Fixed-rate semantics: if an execution overruns `period`, missed runs
134    /// are executed back-to-back to catch up — they are never skipped.
135    ///
136    /// # Panics
137    ///
138    /// Panics if `period` is zero.
139    fn schedule_periodically(
140        &self,
141        mut task: impl FnMut(usize) -> bool + MaybeSend + 'static,
142        period: Duration,
143        delay: Option<Duration>,
144    ) -> BoundDropDisposal<Self::D>
145    where
146        Self: Clone + MaybeSend + 'static,
147    {
148        assert!(!period.is_zero(), "period must be non-zero");
149        let mut next_time = Instant::now() + delay.unwrap_or_default();
150        self.schedule_recursively(
151            move |count| {
152                let r#continue = task(count);
153                if r#continue {
154                    next_time += period;
155                    RecursionAction::ContinueAt(next_time)
156                } else {
157                    RecursionAction::Stop
158                }
159            },
160            delay,
161        )
162    }
163
164    /// Drives `stream` to completion, invoking `result_callback` with
165    /// `Some(item)` for each element and a final `None` when the stream ends.
166    ///
167    /// The callback's answer is what keeps the stream running: returning
168    /// `false` stops polling it right there, and the final `None` is then
169    /// never delivered — the stream is dropped along with the task.
170    ///
171    /// Disposal aborts the task without delivering the final `None`.
172    ///
173    /// The loop yields to the executor after each element (even when the
174    /// stream is always ready), so other tasks can make progress and
175    /// disposal can take effect.
176    #[cfg(feature = "futures")]
177    fn schedule_stream<SM>(
178        &self,
179        stream: SM,
180        mut result_callback: impl FnMut(Option<SM::Item>) -> bool + MaybeSend + 'static,
181    ) -> BoundDropDisposal<Self::D>
182    where
183        SM: Stream + MaybeSend + 'static,
184    {
185        self.spawn_future(async move {
186            let mut stream = std::pin::pin!(stream);
187            loop {
188                // A `while let` would keep the `Option<Item>` temporary alive
189                // across the yield below, requiring `SM::Item: Send`.
190                match stream.next().await {
191                    Some(item) => {
192                        if !result_callback(Some(item)) {
193                            return;
194                        }
195                    }
196                    None => break,
197                }
198                // Yield so other tasks can run and disposal can take effect,
199                // even when the stream is always ready.
200                YieldNow(false).await;
201            }
202            let _ = result_callback(None);
203        })
204    }
205}