Skip to main content

qframe/runtime/
task.rs

1//! Background tasks with progress, cancellation and an outcome, and the model that tracks them
2//! for display.
3//!
4//! A [`Task`] runs on its own thread and talks to the application only through messages, so
5//! drawing never waits for it. The application keeps a [`Tasks`] model up to date from
6//! [`TaskEvent`]s and shows it, for example with [`TaskList`](crate::widgets::TaskList).
7
8use std::collections::{HashMap, HashSet};
9use std::io;
10use std::panic::{AssertUnwindSafe, catch_unwind};
11use std::sync::atomic::{AtomicU64, Ordering};
12use std::sync::mpsc::Sender;
13use std::sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError};
14use std::time::{Duration, Instant};
15
16use super::command::MapFn;
17
18/// Identifies one task for its whole life. Ids are unique within the process.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
20pub struct TaskId(u64);
21
22impl TaskId {
23    fn next() -> Self {
24        static NEXT: AtomicU64 = AtomicU64::new(1);
25        Self(NEXT.fetch_add(1, Ordering::Relaxed))
26    }
27}
28
29/// How a task ended.
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub enum TaskOutcome {
32    /// The work returned `Ok`; its message was delivered just before this outcome.
33    Done,
34    /// The work returned `Err` with this reason, or panicked.
35    Failed(String),
36    /// [`Command::cancel_task`](crate::runtime::Command::cancel_task) asked it to stop; its
37    /// result was dropped.
38    Cancelled,
39}
40
41/// What happened to a task, delivered through [`Task::on_event`].
42#[derive(Debug, Clone, PartialEq)]
43pub enum TaskEvent {
44    /// The task was started with this label.
45    Started {
46        /// The task.
47        id: TaskId,
48        /// Its label.
49        label: String,
50    },
51    /// The task reported progress. `None` fields keep their previous value.
52    Progress {
53        /// The task.
54        id: TaskId,
55        /// Completed share from 0 to 1.
56        fraction: Option<f32>,
57        /// A short note about the current step.
58        note: Option<String>,
59    },
60    /// The task ended.
61    Finished {
62        /// The task.
63        id: TaskId,
64        /// How.
65        outcome: TaskOutcome,
66    },
67}
68
69impl TaskEvent {
70    /// The task the event is about.
71    #[must_use]
72    pub fn id(&self) -> TaskId {
73        match self {
74            Self::Started { id, .. } | Self::Progress { id, .. } | Self::Finished { id, .. } => *id,
75        }
76    }
77}
78
79type Work<Msg> = Box<dyn FnOnce(&TaskCx<Msg>) -> Result<Msg, String> + Send>;
80type EventMessage<Msg> = Arc<dyn Fn(TaskEvent) -> Msg + Send + Sync>;
81/// Hands a message of running work to the event loop.
82type Deliver<Msg> = Arc<dyn Fn(Msg) + Send + Sync>;
83/// Hands a task event, already turned into a message, to the event loop.
84type Report = Arc<dyn Fn(TaskEvent) + Send + Sync>;
85
86/// Work to run in the background with progress, cancellation and an outcome.
87///
88/// ```
89/// use std::time::Duration;
90/// use qframe::runtime::{Command, Task, TaskEvent};
91///
92/// enum Msg {
93///     Task(TaskEvent),
94///     Built(String),
95/// }
96///
97/// let task = Task::new("Build image", |cx| {
98///     for step in 0..4 {
99///         if !cx.sleep(Duration::from_millis(300)) {
100///             return Err("stopped".into());
101///         }
102///         cx.progress((step + 1) as f32 / 4.0);
103///     }
104///     Ok(Msg::Built("sha256:4f2a".into()))
105/// })
106/// .on_event(Msg::Task);
107/// let id = task.id(); // keep it to cancel the task later
108/// let command: Command<Msg> = Command::task(task);
109/// # let _ = (id, command);
110/// ```
111pub struct Task<Msg> {
112    id: TaskId,
113    label: String,
114    work: Work<Msg>,
115    on_event: Option<EventMessage<Msg>>,
116}
117
118impl<Msg: Send + 'static> Task<Msg> {
119    /// A task labelled `label` running `work`. `Ok` delivers its message; `Err` fails the task
120    /// with a reason.
121    #[must_use]
122    pub fn new(
123        label: impl Into<String>,
124        work: impl FnOnce(&TaskCx<Msg>) -> Result<Msg, String> + Send + 'static,
125    ) -> Self {
126        Self { id: TaskId::next(), label: label.into(), work: Box::new(work), on_event: None }
127    }
128
129    /// Turns start, progress and the outcome into messages, e.g. to update a [`Tasks`] model.
130    #[must_use]
131    pub fn on_event(mut self, message: impl Fn(TaskEvent) -> Msg + Send + Sync + 'static) -> Self {
132        self.on_event = Some(Arc::new(message));
133        self
134    }
135
136    /// The task's id, known before it starts.
137    #[must_use]
138    pub fn id(&self) -> TaskId {
139        self.id
140    }
141
142    /// The task's label.
143    #[must_use]
144    pub fn label(&self) -> &str {
145        &self.label
146    }
147
148    /// The same task delivering `map(message)` for every message it would deliver: its result,
149    /// what the work sends while it runs and its events.
150    pub(crate) fn map<B: Send + 'static>(self, map: MapFn<Msg, B>) -> Task<B> {
151        let Self { id, label, work, on_event } = self;
152        let on_event = on_event.map(|message| {
153            let map = Arc::clone(&map);
154            Arc::new(move |event| map(message(event))) as EventMessage<B>
155        });
156        let work: Work<B> = Box::new(move |cx: &TaskCx<B>| {
157            let deliver = Arc::clone(&cx.deliver);
158            let inner_map = Arc::clone(&map);
159            let inner = TaskCx {
160                id: cx.id,
161                clock: Arc::clone(&cx.clock),
162                deliver: Arc::new(move |message| deliver(inner_map(message))),
163                report: cx.report.clone(),
164            };
165            work(&inner).map(|message| map(message))
166        });
167        Task { id, label, work, on_event }
168    }
169}
170
171/// A message from a background thread to the event loop.
172pub(crate) enum Delivery<Msg> {
173    /// Apply this message.
174    Message(Msg),
175    /// A piece of background work ended.
176    Ended,
177}
178
179/// Time as tasks see it: the real clock, or the test harness's fake clock that only moves when
180/// the test advances it.
181pub(crate) struct TaskClock {
182    fake: bool,
183    state: Mutex<ClockState>,
184    changed: Condvar,
185}
186
187#[derive(Default)]
188struct ClockState {
189    now: Duration,
190    /// Tasks that are working rather than sleeping.
191    busy: usize,
192    cancelled: HashSet<TaskId>,
193    /// Fake clock only: tasks asleep and when they wake. Whoever wakes a sleeper (the clock
194    /// moving or a cancel) counts it as busy right away, so settling never misses it.
195    sleeping: HashMap<TaskId, Duration>,
196    /// Fake clock only: where each task is in time. A task woken by a big clock jump carries on
197    /// from the moment its sleep ended, so a loop of sleeps keeps its rhythm.
198    task_time: HashMap<TaskId, Duration>,
199}
200
201/// How long the harness waits for tasks to reach a sleep before it gives up.
202const SETTLE_LIMIT: Duration = Duration::from_secs(10);
203
204impl TaskClock {
205    pub(crate) fn new(fake: bool) -> Arc<Self> {
206        Arc::new(Self { fake, state: Mutex::new(ClockState::default()), changed: Condvar::new() })
207    }
208
209    fn lock(&self) -> MutexGuard<'_, ClockState> {
210        self.state.lock().unwrap_or_else(PoisonError::into_inner)
211    }
212
213    /// Asks task `id` to stop; its sleeps return at once.
214    pub(crate) fn cancel(&self, id: TaskId) {
215        let mut state = self.lock();
216        state.cancelled.insert(id);
217        if state.sleeping.remove(&id).is_some() {
218            state.busy += 1;
219            let now = state.now;
220            state.task_time.insert(id, now);
221        }
222        drop(state);
223        self.changed.notify_all();
224    }
225
226    /// Moves the fake clock to `now` and waits until every task is asleep or finished.
227    ///
228    /// # Panics
229    ///
230    /// Panics when a task keeps working for longer than ten seconds, which in a test means it
231    /// never sleeps or blocks forever.
232    pub(crate) fn settle(&self, now: Duration) {
233        let mut state = self.lock();
234        state.now = state.now.max(now);
235        let current = state.now;
236        let due: Vec<(TaskId, Duration)> =
237            state.sleeping.iter().filter(|(_, until)| **until <= current).map(|(id, until)| (*id, *until)).collect();
238        for (id, until) in due {
239            state.sleeping.remove(&id);
240            state.task_time.insert(id, until);
241            state.busy += 1;
242        }
243        self.changed.notify_all();
244        let started = Instant::now();
245        while state.busy > 0 {
246            let waited = started.elapsed();
247            assert!(waited < SETTLE_LIMIT, "a background task kept working for {SETTLE_LIMIT:?} without sleeping");
248            state = self.changed.wait_timeout(state, SETTLE_LIMIT - waited).unwrap_or_else(PoisonError::into_inner).0;
249        }
250    }
251
252    fn is_cancelled(&self, id: TaskId) -> bool {
253        self.lock().cancelled.contains(&id)
254    }
255
256    fn begin(&self, id: TaskId) {
257        let mut state = self.lock();
258        state.busy += 1;
259        let now = state.now;
260        state.task_time.insert(id, now);
261    }
262
263    fn end(&self, id: TaskId) {
264        let mut state = self.lock();
265        state.busy = state.busy.saturating_sub(1);
266        state.cancelled.remove(&id);
267        state.task_time.remove(&id);
268        drop(state);
269        self.changed.notify_all();
270    }
271
272    /// Sleeps task `id` for `duration`; returns `false` when it was cancelled.
273    fn sleep(&self, id: TaskId, duration: Duration) -> bool {
274        let mut state = self.lock();
275        if self.fake {
276            let until = state.task_time.get(&id).copied().unwrap_or(state.now) + duration;
277            if until <= state.now {
278                state.task_time.insert(id, until);
279            } else if !state.cancelled.contains(&id) {
280                state.sleeping.insert(id, until);
281                state.busy = state.busy.saturating_sub(1);
282                self.changed.notify_all();
283                while state.sleeping.contains_key(&id) {
284                    state = self.changed.wait(state).unwrap_or_else(PoisonError::into_inner);
285                }
286            }
287        } else {
288            let deadline = Instant::now() + duration;
289            while !state.cancelled.contains(&id) {
290                let left = deadline.saturating_duration_since(Instant::now());
291                if left.is_zero() {
292                    break;
293                }
294                state = self.changed.wait_timeout(state, left).unwrap_or_else(PoisonError::into_inner).0;
295            }
296        }
297        !state.cancelled.contains(&id)
298    }
299}
300
301/// What running work can do: report progress, send messages, notice cancellation and sleep.
302pub struct TaskCx<Msg> {
303    id: TaskId,
304    clock: Arc<TaskClock>,
305    deliver: Deliver<Msg>,
306    /// Events go out as messages of the application's type, which a task built for another
307    /// message type ([`Command::map`](crate::runtime::Command::map)) does not know.
308    report: Option<Report>,
309}
310
311impl<Msg: Send + 'static> TaskCx<Msg> {
312    /// The running task.
313    #[must_use]
314    pub fn id(&self) -> TaskId {
315        self.id
316    }
317
318    /// Reports the completed share, from 0 to 1.
319    pub fn progress(&self, fraction: f32) {
320        self.event(TaskEvent::Progress { id: self.id, fraction: Some(fraction.clamp(0.0, 1.0)), note: None });
321    }
322
323    /// Describes the current step, e.g. "pushing layers".
324    pub fn note(&self, note: impl Into<String>) {
325        self.event(TaskEvent::Progress { id: self.id, fraction: None, note: Some(note.into()) });
326    }
327
328    /// Delivers `message` to the application while the work goes on, e.g. a log line.
329    pub fn send(&self, message: Msg) {
330        (self.deliver)(message);
331    }
332
333    /// Whether the application asked this task to stop. Long work should check it and return.
334    #[must_use]
335    pub fn is_cancelled(&self) -> bool {
336        self.clock.is_cancelled(self.id)
337    }
338
339    /// Waits `duration`, waking early when the task is cancelled. Returns `false` when it was
340    /// cancelled. In tests the harness's fake clock decides when the sleep ends.
341    #[must_use]
342    pub fn sleep(&self, duration: Duration) -> bool {
343        self.clock.sleep(self.id, duration)
344    }
345
346    fn event(&self, event: TaskEvent) {
347        if let Some(report) = &self.report {
348            report(event);
349        }
350    }
351}
352
353/// Starts a thread named `name` running `run`. A failed start drops `run`.
354pub(crate) type Spawner = fn(String, Box<dyn FnOnce() + Send>) -> io::Result<()>;
355
356/// The [`Spawner`] of the runtime: a real thread.
357pub(crate) fn spawn_thread(name: String, run: Box<dyn FnOnce() + Send>) -> io::Result<()> {
358    std::thread::Builder::new().name(name).spawn(run).map(drop)
359}
360
361/// The reason of a task whose thread could not start.
362const NO_THREAD: &str = "could not start a thread";
363
364/// Starts `task` on a thread. The `Started` message is returned for the caller to apply at once.
365/// When no thread can start, the task fails at once and still ends.
366pub(crate) fn spawn<Msg: Send + 'static>(
367    task: Task<Msg>,
368    clock: &Arc<TaskClock>,
369    sender: &Sender<Delivery<Msg>>,
370    spawner: Spawner,
371) -> Option<Msg> {
372    let Task { id, label, work, on_event } = task;
373    let started = on_event.as_ref().map(|message| message(TaskEvent::Started { id, label: label.clone() }));
374    let failed = on_event.clone();
375    let outlet = sender.clone();
376    let deliver: Deliver<Msg> = Arc::new(move |message| {
377        let _ = outlet.send(Delivery::Message(message));
378    });
379    let report = on_event.map(|message| {
380        let deliver = Arc::clone(&deliver);
381        Arc::new(move |event| deliver(message(event))) as Report
382    });
383    let cx = TaskCx { id, clock: Arc::clone(clock), deliver, report };
384    let ended = sender.clone();
385    clock.begin(id);
386    let run = Box::new(move || {
387        let result = catch_unwind(AssertUnwindSafe(|| work(&cx)))
388            .unwrap_or_else(|_| Err(format!("the task `{label}` panicked")));
389        let outcome = match result {
390            _ if cx.is_cancelled() => TaskOutcome::Cancelled,
391            Ok(message) => {
392                cx.send(message);
393                TaskOutcome::Done
394            }
395            Err(reason) => TaskOutcome::Failed(reason),
396        };
397        // The event's message is the application's code (and a conversion of `Command::map`);
398        // if it panics there is nothing left to tell, but the task must still end, or the
399        // runtime would wait for it forever.
400        let _ = catch_unwind(AssertUnwindSafe(|| cx.event(TaskEvent::Finished { id, outcome })));
401        let _ = ended.send(Delivery::Ended);
402        cx.clock.end(id);
403    });
404    if spawner(format!("quvyta-task-{}", id.0), run).is_err() {
405        // The work went with the closure; report the failure so the task never stays running.
406        clock.end(id);
407        if let Some(message) = failed {
408            let outcome = TaskOutcome::Failed(NO_THREAD.to_owned());
409            let _ = sender.send(Delivery::Message(message(TaskEvent::Finished { id, outcome })));
410        }
411        let _ = sender.send(Delivery::Ended);
412    }
413    started
414}
415
416/// One task as the application shows it.
417#[derive(Debug, Clone, PartialEq)]
418pub struct TaskEntry {
419    /// The task.
420    pub id: TaskId,
421    /// Its label.
422    pub label: String,
423    /// Completed share from 0 to 1, when the task reports one.
424    pub fraction: Option<f32>,
425    /// The latest note.
426    pub note: Option<String>,
427    /// How it ended; `None` while running.
428    pub outcome: Option<TaskOutcome>,
429}
430
431/// Tasks an application shows, kept up to date with [`Tasks::apply`]. Newest last.
432#[derive(Debug, Clone, Default, PartialEq)]
433pub struct Tasks {
434    entries: Vec<TaskEntry>,
435}
436
437impl Tasks {
438    /// No tasks.
439    #[must_use]
440    pub fn new() -> Self {
441        Self::default()
442    }
443
444    /// Records `event`.
445    pub fn apply(&mut self, event: &TaskEvent) {
446        match event {
447            TaskEvent::Started { id, label } => {
448                self.entries.retain(|entry| entry.id != *id);
449                self.entries.push(TaskEntry {
450                    id: *id,
451                    label: label.clone(),
452                    fraction: None,
453                    note: None,
454                    outcome: None,
455                });
456            }
457            TaskEvent::Progress { id, fraction, note } => {
458                if let Some(entry) = self.entries.iter_mut().find(|entry| entry.id == *id) {
459                    entry.fraction = fraction.or(entry.fraction);
460                    if note.is_some() {
461                        entry.note.clone_from(note);
462                    }
463                }
464            }
465            TaskEvent::Finished { id, outcome } => {
466                if let Some(entry) = self.entries.iter_mut().find(|entry| entry.id == *id) {
467                    entry.outcome = Some(outcome.clone());
468                }
469            }
470        }
471    }
472
473    /// Every task, oldest first.
474    #[must_use]
475    pub fn entries(&self) -> &[TaskEntry] {
476        &self.entries
477    }
478
479    /// The task `id`.
480    #[must_use]
481    pub fn get(&self, id: TaskId) -> Option<&TaskEntry> {
482        self.entries.iter().find(|entry| entry.id == id)
483    }
484
485    /// How many tasks are still running.
486    #[must_use]
487    pub fn running(&self) -> usize {
488        self.entries.iter().filter(|entry| entry.outcome.is_none()).count()
489    }
490
491    /// Forgets finished tasks.
492    pub fn clear_finished(&mut self) {
493        self.entries.retain(|entry| entry.outcome.is_none());
494    }
495}
496
497#[cfg(test)]
498mod tests {
499    use super::*;
500    use crate::runtime::engine::{Engine, TaskMode};
501    use crate::runtime::{App, Command, Harness};
502    use crate::widget::View;
503    use crate::widgets::Text;
504
505    #[derive(Default)]
506    struct Pipeline {
507        tasks: Tasks,
508        built: Option<String>,
509        lines: Vec<String>,
510        build: Option<TaskId>,
511    }
512
513    enum Msg {
514        Build,
515        Cancel,
516        Fail,
517        Panic,
518        Task(TaskEvent),
519        Built(String),
520        Line(String),
521    }
522
523    impl App for Pipeline {
524        type Msg = Msg;
525        fn update(&mut self, msg: Msg) -> Command<Msg> {
526            match msg {
527                Msg::Build => {
528                    let task = Task::new("Build image", |cx| {
529                        cx.note("resolving layers");
530                        for step in 0..4 {
531                            if !cx.sleep(Duration::from_millis(100)) {
532                                return Err("stopped".into());
533                            }
534                            cx.progress((step + 1) as f32 / 4.0);
535                            cx.send(Msg::Line(format!("layer {step}")));
536                        }
537                        Ok(Msg::Built("sha256:4f2a".into()))
538                    })
539                    .on_event(Msg::Task);
540                    self.build = Some(task.id());
541                    return Command::task(task);
542                }
543                Msg::Cancel => return self.build.map_or_else(Command::none, Command::cancel_task),
544                Msg::Fail => {
545                    return Command::task(
546                        Task::new("Sync registry", |cx| {
547                            let _ = cx.sleep(Duration::from_millis(50));
548                            Err("registry timed out".into())
549                        })
550                        .on_event(Msg::Task),
551                    );
552                }
553                Msg::Panic => {
554                    return Command::task(
555                        Task::new("Broken", |_| -> Result<Msg, String> { panic!("boom") }).on_event(Msg::Task),
556                    );
557                }
558                Msg::Task(event) => self.tasks.apply(&event),
559                Msg::Built(digest) => self.built = Some(digest),
560                Msg::Line(line) => self.lines.push(line),
561            }
562            Command::none()
563        }
564        fn view(&self, ui: &mut View<'_, Msg>) {
565            ui.add(Text::new(format!("running {}", self.tasks.running())));
566        }
567    }
568
569    #[test]
570    fn progress_follows_the_fake_clock_and_completes() {
571        let mut h = Harness::new(Pipeline::default(), 20, 1);
572        h.send(Msg::Build);
573        assert_eq!(h.screen(), "running 1\n");
574        let entry = h.app().tasks.entries()[0].clone();
575        assert_eq!(entry.label, "Build image");
576        assert_eq!(entry.note.as_deref(), Some("resolving layers"));
577        assert_eq!(entry.fraction, None);
578        h.advance(Duration::from_millis(100));
579        assert_eq!(h.app().tasks.entries()[0].fraction, Some(0.25));
580        assert_eq!(h.app().lines, ["layer 0"]);
581        h.advance(Duration::from_millis(250));
582        assert_eq!(h.app().tasks.entries()[0].fraction, Some(0.75));
583        h.advance(Duration::from_millis(100));
584        assert_eq!(h.app().built.as_deref(), Some("sha256:4f2a"));
585        assert_eq!(h.app().tasks.entries()[0].outcome, Some(TaskOutcome::Done));
586        assert_eq!(h.screen(), "running 0\n");
587    }
588
589    #[test]
590    fn cancelling_wakes_the_sleep_and_drops_the_result() {
591        let mut h = Harness::new(Pipeline::default(), 20, 1);
592        h.send(Msg::Build).advance(Duration::from_millis(150)).send(Msg::Cancel);
593        let entry = &h.app().tasks.entries()[0];
594        assert_eq!(entry.outcome, Some(TaskOutcome::Cancelled));
595        assert_eq!(entry.fraction, Some(0.25));
596        assert!(h.app().built.is_none());
597    }
598
599    fn no_thread(_: String, _: Box<dyn FnOnce() + Send>) -> io::Result<()> {
600        Err(io::Error::other("no threads left"))
601    }
602
603    #[test]
604    fn a_task_whose_thread_cannot_start_fails_and_ends() {
605        let mut engine = Engine::new(Pipeline::default(), crate::env::Env::builtin(), TaskMode::Threads);
606        engine.spawner = no_thread;
607        engine.update(Msg::Build);
608        assert_eq!(engine.poll_tasks(), 2, "Finished, then Ended");
609        let entry = &engine.app.tasks.entries()[0];
610        assert_eq!(entry.outcome, Some(TaskOutcome::Failed("could not start a thread".into())));
611        assert_eq!((engine.app.tasks.running(), engine.pending_tasks), (0, 0));
612        assert!(engine.app.built.is_none());
613    }
614
615    /// Starts, on `Some(())`, a task whose event message panics once the task finishes.
616    struct Fragile;
617
618    impl App for Fragile {
619        type Msg = Option<()>;
620        fn update(&mut self, start: Option<()>) -> Command<Option<()>> {
621            if start.is_none() {
622                return Command::none();
623            }
624            Command::task(Task::new("Fragile", |_| Ok(None)).on_event(|event| match event {
625                TaskEvent::Finished { .. } => panic!("the message of the outcome failed"),
626                _ => None,
627            }))
628        }
629        fn view(&self, ui: &mut View<'_, Option<()>>) {
630            ui.add(Text::new("fragile"));
631        }
632    }
633
634    #[test]
635    fn a_task_whose_last_event_message_panics_still_ends() {
636        let mut engine = Engine::new(Fragile, crate::env::Env::builtin(), TaskMode::Threads);
637        engine.update(Some(()));
638        let started = Instant::now();
639        while engine.pending_tasks > 0 {
640            assert!(started.elapsed() < Duration::from_secs(10), "the runtime waits for the task forever");
641            engine.poll_tasks();
642            std::thread::sleep(Duration::from_millis(5));
643        }
644    }
645
646    #[test]
647    fn failures_and_panics_become_outcomes() {
648        let mut h = Harness::new(Pipeline::default(), 20, 1);
649        h.send(Msg::Fail).send(Msg::Panic);
650        assert_eq!(h.app().tasks.running(), 1, "the failing task still sleeps");
651        assert_eq!(h.app().tasks.entries()[1].outcome, Some(TaskOutcome::Failed("the task `Broken` panicked".into())));
652        h.advance(Duration::from_millis(50));
653        assert_eq!(h.app().tasks.entries()[0].outcome, Some(TaskOutcome::Failed("registry timed out".into())));
654        let mut tasks = h.app().tasks.clone();
655        tasks.clear_finished();
656        assert!(tasks.entries().is_empty());
657    }
658}