Skip to main content

urushi_tui_app/
executor.rs

1//! Execution of effects and reconciliation of long-lived sources.
2//!
3//! The application-facing values name no executor. This module is the runtime
4//! side of that boundary: it hands work to an [`Executor`], owns the returned
5//! cancellation handles, and sends completed messages into the one
6//! [`DeliveryQueue`]. A handle is kept for exactly as long as the runtime may
7//! still accept that execution's completion.
8
9use std::collections::{HashMap, HashSet};
10use std::future::Future;
11use std::io;
12use std::pin::Pin;
13use std::sync::Arc;
14use std::sync::atomic::{AtomicBool, Ordering};
15use std::time::{Duration, Instant};
16
17use urushi::Key;
18
19use super::delivery::{DeliveryQueue, EffectCancellation};
20use super::effect::{Effect, EffectKind};
21use super::subscription::{Source, Subscription};
22
23/// Asynchronous work accepted by a runtime executor.
24pub type Task = Pin<Box<dyn Future<Output = ()> + Send + 'static>>;
25
26/// Blocking work accepted by a runtime executor.
27pub type BlockingTask = Box<dyn FnOnce() + Send + 'static>;
28
29/// A running task whose drop cancels work that has not completed yet.
30pub trait Execution: Send {}
31
32/// Where runtime work runs, expressed without an executor's task type.
33pub trait Executor: Send + Sync + 'static {
34    /// Starts an asynchronous task and returns its cancellation handle.
35    fn spawn(&self, task: Task) -> Box<dyn Execution>;
36
37    /// Starts blocking work off the application thread.
38    fn spawn_blocking(&self, task: BlockingTask) -> Box<dyn Execution>;
39}
40
41impl<E> Executor for Arc<E>
42where
43    E: Executor + ?Sized,
44{
45    fn spawn(&self, task: Task) -> Box<dyn Execution> {
46        E::spawn(self, task)
47    }
48
49    fn spawn_blocking(&self, task: BlockingTask) -> Box<dyn Execution> {
50        E::spawn_blocking(self, task)
51    }
52}
53
54/// The source of time shared by delayed effects and interval subscriptions.
55pub trait Clock: Send + Sync + 'static {
56    /// Returns the clock's current monotonic instant.
57    fn now(&self) -> Instant;
58
59    /// Waits for `duration` and returns the firing instant.
60    fn sleep(&self, duration: Duration) -> Pin<Box<dyn Future<Output = Instant> + Send + 'static>>;
61}
62
63impl<C> Clock for Arc<C>
64where
65    C: Clock + ?Sized,
66{
67    fn now(&self) -> Instant {
68        C::now(self)
69    }
70
71    fn sleep(&self, duration: Duration) -> Pin<Box<dyn Future<Output = Instant> + Send + 'static>> {
72        C::sleep(self, duration)
73    }
74}
75
76/// Tokio's task runner, kept behind Urushi's executor vocabulary.
77#[cfg_attr(test, allow(dead_code, reason = "wired by the runtime core"))]
78pub struct TokioExecutor {
79    handle: tokio::runtime::Handle,
80}
81
82impl TokioExecutor {
83    #[cfg_attr(test, allow(dead_code, reason = "wired by the runtime core"))]
84    /// Uses `handle` for asynchronous and blocking runtime work.
85    pub fn new(handle: tokio::runtime::Handle) -> Self {
86        Self { handle }
87    }
88}
89
90#[cfg_attr(test, allow(dead_code, reason = "wired by the runtime core"))]
91/// The system monotonic clock driven by Tokio timers.
92pub struct TokioClock;
93
94impl Clock for TokioClock {
95    fn now(&self) -> Instant {
96        Instant::now()
97    }
98
99    fn sleep(&self, duration: Duration) -> Pin<Box<dyn Future<Output = Instant> + Send + 'static>> {
100        let deadline = Instant::now() + duration;
101        Box::pin(async move {
102            tokio::time::sleep_until(tokio::time::Instant::from_std(deadline)).await;
103            deadline
104        })
105    }
106}
107
108impl Executor for TokioExecutor {
109    fn spawn(&self, task: Task) -> Box<dyn Execution> {
110        Box::new(TokioExecution(self.handle.spawn(task)))
111    }
112
113    fn spawn_blocking(&self, task: BlockingTask) -> Box<dyn Execution> {
114        Box::new(TokioExecution(self.handle.spawn_blocking(task)))
115    }
116}
117
118impl Executor for tokio::runtime::Handle {
119    fn spawn(&self, task: Task) -> Box<dyn Execution> {
120        Box::new(TokioExecution(self.spawn(task)))
121    }
122
123    fn spawn_blocking(&self, task: BlockingTask) -> Box<dyn Execution> {
124        Box::new(TokioExecution(self.spawn_blocking(task)))
125    }
126}
127
128#[cfg_attr(test, allow(dead_code, reason = "constructed by TokioExecutor"))]
129struct TokioExecution(tokio::task::JoinHandle<()>);
130
131impl Execution for TokioExecution {}
132
133impl Drop for TokioExecution {
134    fn drop(&mut self) {
135        self.0.abort();
136    }
137}
138
139/// Whether interpreting an effect asks the runtime core to continue or stop.
140#[derive(Clone, Copy, Debug, PartialEq, Eq)]
141pub(crate) enum EffectControl {
142    Continue,
143    Shutdown,
144}
145
146/// Interprets effects and owns every execution whose result is still live.
147pub(crate) struct EffectExecutor<Message> {
148    executor: Arc<dyn Executor>,
149    clock: Arc<dyn Clock>,
150    deliveries: DeliveryQueue<Message>,
151    ordinary: Vec<Running>,
152    latest: HashMap<Key, Running>,
153}
154
155impl<Message: Send + 'static> EffectExecutor<Message> {
156    pub(crate) fn new(
157        executor: Arc<dyn Executor>,
158        clock: Arc<dyn Clock>,
159        deliveries: DeliveryQueue<Message>,
160    ) -> Self {
161        Self {
162            executor,
163            clock,
164            deliveries,
165            ordinary: Vec::new(),
166            latest: HashMap::new(),
167        }
168    }
169
170    /// Starts every member in source order, unless the tree requests shutdown.
171    ///
172    /// Shutdown is found before anything starts so a batch containing it cannot
173    /// start a sibling merely because that sibling appeared first.
174    pub(crate) fn start(&mut self, effect: Effect<Message>) -> EffectControl {
175        self.reap_finished();
176        if effect.requests_shutdown() {
177            self.stop();
178            return EffectControl::Shutdown;
179        }
180        let kind = effect.into_kind();
181        self.start_kind(kind);
182        self.reap_finished();
183        EffectControl::Continue
184    }
185
186    /// Suppresses every completion the runtime still knows to be in flight.
187    pub(crate) fn stop(&mut self) {
188        self.latest.clear();
189        self.ordinary.clear();
190    }
191
192    fn start_kind(&mut self, kind: EffectKind<Message>) {
193        match kind {
194            EffectKind::None => {}
195            EffectKind::Shutdown => unreachable!("shutdown is handled before effects start"),
196            EffectKind::Perform { key, work } => {
197                let executor = Arc::clone(&self.executor);
198                self.start_one(key, move |completion, finished| {
199                    executor.spawn_blocking(Box::new(move || {
200                        let _finished = FinishOnDrop(finished);
201                        completion.complete(work());
202                    }))
203                });
204            }
205            EffectKind::Future { key, future } => {
206                let executor = Arc::clone(&self.executor);
207                self.start_one(key, move |completion, finished| {
208                    executor.spawn(Box::pin(async move {
209                        let _finished = FinishOnDrop(finished);
210                        completion.complete(future.await);
211                    }))
212                });
213            }
214            EffectKind::After { key, delay, fire } => {
215                let executor = Arc::clone(&self.executor);
216                let clock = Arc::clone(&self.clock);
217                self.start_one(key, move |completion, finished| {
218                    executor.spawn(Box::pin(async move {
219                        let _finished = FinishOnDrop(finished);
220                        let at = clock.sleep(delay).await;
221                        completion.complete(fire(at));
222                    }))
223                });
224            }
225            EffectKind::Batch(effects) => {
226                for effect in effects {
227                    self.start_kind(effect.into_kind());
228                }
229            }
230        }
231    }
232
233    fn start_one(
234        &mut self,
235        key: Option<Key>,
236        spawn: impl FnOnce(
237            super::delivery::EffectCompletion<Message>,
238            Arc<AtomicBool>,
239        ) -> Box<dyn Execution>,
240    ) {
241        if let Some(key) = key {
242            // Cancellation must linearize before the replacement can run.
243            drop(self.latest.remove(&key));
244            let running = Running::start(&self.deliveries, spawn);
245            self.latest.insert(key, running);
246        } else {
247            self.ordinary.push(Running::start(&self.deliveries, spawn));
248        }
249    }
250
251    fn reap_finished(&mut self) {
252        self.ordinary.retain(|running| !running.is_finished());
253        self.latest.retain(|_, running| !running.is_finished());
254    }
255}
256
257struct Running {
258    // Drop the permit before the task handle: a blocking task may finish while
259    // cancellation is in progress, and its completion must see Canceled first.
260    _cancellation: EffectCancellation,
261    _execution: Box<dyn Execution>,
262    finished: Arc<AtomicBool>,
263}
264
265/// Marks a task finished on normal return and while unwinding from its work.
266struct FinishOnDrop(Arc<AtomicBool>);
267
268impl Drop for FinishOnDrop {
269    fn drop(&mut self) {
270        self.0.store(true, Ordering::Release);
271    }
272}
273
274impl Running {
275    fn start<Message>(
276        deliveries: &DeliveryQueue<Message>,
277        spawn: impl FnOnce(
278            super::delivery::EffectCompletion<Message>,
279            Arc<AtomicBool>,
280        ) -> Box<dyn Execution>,
281    ) -> Self {
282        let (completion, cancellation) = deliveries.latest_completion();
283        let finished = Arc::new(AtomicBool::new(false));
284        let execution = spawn(completion, Arc::clone(&finished));
285        Self {
286            _cancellation: cancellation,
287            _execution: execution,
288            finished,
289        }
290    }
291
292    fn is_finished(&self) -> bool {
293        self.finished.load(Ordering::Acquire)
294    }
295}
296
297/// Starts a source and owns whatever must be dropped to stop it.
298pub(crate) trait SourceSpawner<Message>: Send + Sync + 'static {
299    fn start(
300        &self,
301        source: Source<Message>,
302        deliveries: DeliveryQueue<Message>,
303    ) -> io::Result<Box<dyn RunningSource<Message>>>;
304
305    fn failure(&self) -> Pin<Box<dyn Future<Output = io::Error> + Send + '_>> {
306        Box::pin(std::future::pending())
307    }
308
309    fn attach_runtime(&self, _deliveries: DeliveryQueue<Message>) {}
310}
311
312/// One running source. Refreshing changes its declaration without restarting it.
313pub(crate) trait RunningSource<Message>: Send {
314    fn refresh(&mut self, source: Source<Message>);
315}
316
317/// Reconciles declarations against the exact keys currently running.
318pub(crate) struct SubscriptionExecutor<Message> {
319    spawner: Arc<dyn SourceSpawner<Message>>,
320    deliveries: DeliveryQueue<Message>,
321    running: HashMap<Key, Box<dyn RunningSource<Message>>>,
322}
323
324impl<Message: Send + 'static> SubscriptionExecutor<Message> {
325    pub(crate) fn new(
326        spawner: Arc<dyn SourceSpawner<Message>>,
327        deliveries: DeliveryQueue<Message>,
328    ) -> Self {
329        Self {
330            spawner,
331            deliveries,
332            running: HashMap::new(),
333        }
334    }
335
336    pub(crate) fn reconcile(&mut self, subscription: Subscription<Message>) -> io::Result<()> {
337        let desired = latest_declarations(subscription.into_sources());
338        let desired_keys: HashSet<Key> = desired.iter().map(|source| source.key).collect();
339        self.running.retain(|key, _| desired_keys.contains(key));
340
341        for source in desired {
342            if let Some(running) = self.running.get_mut(&source.key) {
343                running.refresh(source);
344            } else {
345                let key = source.key;
346                let running = self.spawner.start(source, self.deliveries.clone())?;
347                self.running.insert(key, running);
348            }
349        }
350        Ok(())
351    }
352
353    pub(crate) fn stop(&mut self) {
354        self.running.clear();
355    }
356}
357
358impl<Message> Drop for SubscriptionExecutor<Message> {
359    fn drop(&mut self) {
360        // Running sources may synchronously release blocking producers. Drop
361        // them before the spawner that owns and joins those producers.
362        self.running.clear();
363    }
364}
365
366/// Keeps the last declaration for a duplicate key, ordered by those surviving
367/// declarations' positions. This is the same "latest declaration wins" rule
368/// reconciliation applies to an already running source.
369fn latest_declarations<Message>(sources: Vec<Source<Message>>) -> Vec<Source<Message>> {
370    let mut seen = HashSet::new();
371    let mut declarations: Vec<_> = sources
372        .into_iter()
373        .rev()
374        .filter(|source| seen.insert(source.key))
375        .collect();
376    declarations.reverse();
377    // Register input before surface starts the shared terminal reader. This
378    // prevents an initial key from being consumed in the short interval where
379    // a declaration contains both sources but only surface is registered.
380    // Surface still accepts its initial Sync observation before reconciliation
381    // returns and before the scheduler is first invalidated.
382    declarations.sort_by_key(|source| source.kind.is_surface());
383    declarations
384}
385
386#[cfg(test)]
387#[path = "executor_tests.rs"]
388mod tests;