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}