1use 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#[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#[derive(Debug, Clone, PartialEq, Eq)]
31pub enum TaskOutcome {
32 Done,
34 Failed(String),
36 Cancelled,
39}
40
41#[derive(Debug, Clone, PartialEq)]
43pub enum TaskEvent {
44 Started {
46 id: TaskId,
48 label: String,
50 },
51 Progress {
53 id: TaskId,
55 fraction: Option<f32>,
57 note: Option<String>,
59 },
60 Finished {
62 id: TaskId,
64 outcome: TaskOutcome,
66 },
67}
68
69impl TaskEvent {
70 #[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>;
81type Deliver<Msg> = Arc<dyn Fn(Msg) + Send + Sync>;
83type Report = Arc<dyn Fn(TaskEvent) + Send + Sync>;
85
86pub 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 #[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 #[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 #[must_use]
138 pub fn id(&self) -> TaskId {
139 self.id
140 }
141
142 #[must_use]
144 pub fn label(&self) -> &str {
145 &self.label
146 }
147
148 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
171pub(crate) enum Delivery<Msg> {
173 Message(Msg),
175 Ended,
177}
178
179pub(crate) struct TaskClock {
182 fake: bool,
183 state: Mutex<ClockState>,
184 changed: Condvar,
185}
186
187#[derive(Default)]
188struct ClockState {
189 now: Duration,
190 busy: usize,
192 cancelled: HashSet<TaskId>,
193 sleeping: HashMap<TaskId, Duration>,
196 task_time: HashMap<TaskId, Duration>,
199}
200
201const 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 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 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 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
301pub struct TaskCx<Msg> {
303 id: TaskId,
304 clock: Arc<TaskClock>,
305 deliver: Deliver<Msg>,
306 report: Option<Report>,
309}
310
311impl<Msg: Send + 'static> TaskCx<Msg> {
312 #[must_use]
314 pub fn id(&self) -> TaskId {
315 self.id
316 }
317
318 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 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 pub fn send(&self, message: Msg) {
330 (self.deliver)(message);
331 }
332
333 #[must_use]
335 pub fn is_cancelled(&self) -> bool {
336 self.clock.is_cancelled(self.id)
337 }
338
339 #[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
353pub(crate) type Spawner = fn(String, Box<dyn FnOnce() + Send>) -> io::Result<()>;
355
356pub(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
361const NO_THREAD: &str = "could not start a thread";
363
364pub(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));
380 });
381 let report = on_event.map(|message| {
382 let deliver = Arc::clone(&deliver);
383 Arc::new(move |event| deliver(message(event))) as Report
384 });
385 let cx = TaskCx { id, clock: Arc::clone(clock), deliver, report };
386 let ended = sender.clone();
387 clock.begin(id);
388 let run = Box::new(move || {
389 let result = catch_unwind(AssertUnwindSafe(|| work(&cx)))
390 .unwrap_or_else(|_| Err(format!("the task `{label}` panicked")));
391 let outcome = match result {
392 _ if cx.is_cancelled() => TaskOutcome::Cancelled,
393 Ok(message) => {
394 cx.send(message);
395 TaskOutcome::Done
396 }
397 Err(reason) => TaskOutcome::Failed(reason),
398 };
399 let _ = catch_unwind(AssertUnwindSafe(|| cx.event(TaskEvent::Finished { id, outcome })));
403 let _ = ended.send(Delivery::Ended);
404 cx.clock.end(id);
405 });
406 if spawner(format!("quvyta-task-{}", id.0), run).is_err() {
407 clock.end(id);
409 if let Some(message) = failed {
410 let outcome = TaskOutcome::Failed(NO_THREAD.to_owned());
411 let _ = sender.send(Delivery::Message(message(TaskEvent::Finished { id, outcome })));
412 }
413 let _ = sender.send(Delivery::Ended);
417 }
418 started
419}
420
421#[derive(Debug, Clone, PartialEq)]
423pub struct TaskEntry {
424 pub id: TaskId,
426 pub label: String,
428 pub fraction: Option<f32>,
430 pub note: Option<String>,
432 pub outcome: Option<TaskOutcome>,
434}
435
436#[derive(Debug, Clone, Default, PartialEq)]
438pub struct Tasks {
439 entries: Vec<TaskEntry>,
440}
441
442impl Tasks {
443 #[must_use]
445 pub fn new() -> Self {
446 Self::default()
447 }
448
449 pub fn apply(&mut self, event: &TaskEvent) {
451 match event {
452 TaskEvent::Started { id, label } => {
453 self.entries.retain(|entry| entry.id != *id);
454 self.entries.push(TaskEntry {
455 id: *id,
456 label: label.clone(),
457 fraction: None,
458 note: None,
459 outcome: None,
460 });
461 }
462 TaskEvent::Progress { id, fraction, note } => {
463 if let Some(entry) = self.entries.iter_mut().find(|entry| entry.id == *id) {
464 entry.fraction = fraction.or(entry.fraction);
465 if note.is_some() {
466 entry.note.clone_from(note);
467 }
468 }
469 }
470 TaskEvent::Finished { id, outcome } => {
471 if let Some(entry) = self.entries.iter_mut().find(|entry| entry.id == *id) {
472 entry.outcome = Some(outcome.clone());
473 }
474 }
475 }
476 }
477
478 #[must_use]
480 pub fn entries(&self) -> &[TaskEntry] {
481 &self.entries
482 }
483
484 #[must_use]
486 pub fn get(&self, id: TaskId) -> Option<&TaskEntry> {
487 self.entries.iter().find(|entry| entry.id == id)
488 }
489
490 #[must_use]
492 pub fn running(&self) -> usize {
493 self.entries.iter().filter(|entry| entry.outcome.is_none()).count()
494 }
495
496 pub fn clear_finished(&mut self) {
498 self.entries.retain(|entry| entry.outcome.is_none());
499 }
500}
501
502#[cfg(test)]
503mod tests {
504 use super::*;
505 use crate::runtime::engine::{Engine, TaskMode};
506 use crate::runtime::{App, Command, Harness};
507 use crate::widget::View;
508 use crate::widgets::Text;
509
510 #[derive(Default)]
511 struct Pipeline {
512 tasks: Tasks,
513 built: Option<String>,
514 lines: Vec<String>,
515 build: Option<TaskId>,
516 }
517
518 enum Msg {
519 Build,
520 Cancel,
521 Fail,
522 Panic,
523 Task(TaskEvent),
524 Built(String),
525 Line(String),
526 }
527
528 impl App for Pipeline {
529 type Msg = Msg;
530 fn update(&mut self, msg: Msg) -> Command<Msg> {
531 match msg {
532 Msg::Build => {
533 let task = Task::new("Build image", |cx| {
534 cx.note("resolving layers");
535 for step in 0..4 {
536 if !cx.sleep(Duration::from_millis(100)) {
537 return Err("stopped".into());
538 }
539 cx.progress((step + 1) as f32 / 4.0);
540 cx.send(Msg::Line(format!("layer {step}")));
541 }
542 Ok(Msg::Built("sha256:4f2a".into()))
543 })
544 .on_event(Msg::Task);
545 self.build = Some(task.id());
546 return Command::task(task);
547 }
548 Msg::Cancel => return self.build.map_or_else(Command::none, Command::cancel_task),
549 Msg::Fail => {
550 return Command::task(
551 Task::new("Sync registry", |cx| {
552 let _ = cx.sleep(Duration::from_millis(50));
553 Err("registry timed out".into())
554 })
555 .on_event(Msg::Task),
556 );
557 }
558 Msg::Panic => {
559 return Command::task(
560 Task::new("Broken", |_| -> Result<Msg, String> { panic!("boom") }).on_event(Msg::Task),
561 );
562 }
563 Msg::Task(event) => self.tasks.apply(&event),
564 Msg::Built(digest) => self.built = Some(digest),
565 Msg::Line(line) => self.lines.push(line),
566 }
567 Command::none()
568 }
569 fn view(&self, ui: &mut View<'_, Msg>) {
570 ui.add(Text::new(format!("running {}", self.tasks.running())));
571 }
572 }
573
574 #[test]
575 fn progress_follows_the_fake_clock_and_completes() {
576 let mut h = Harness::new(Pipeline::default(), 20, 1);
577 h.send(Msg::Build);
578 assert_eq!(h.screen(), "running 1\n");
579 let entry = h.app().tasks.entries()[0].clone();
580 assert_eq!(entry.label, "Build image");
581 assert_eq!(entry.note.as_deref(), Some("resolving layers"));
582 assert_eq!(entry.fraction, None);
583 h.advance(Duration::from_millis(100));
584 assert_eq!(h.app().tasks.entries()[0].fraction, Some(0.25));
585 assert_eq!(h.app().lines, ["layer 0"]);
586 h.advance(Duration::from_millis(250));
587 assert_eq!(h.app().tasks.entries()[0].fraction, Some(0.75));
588 h.advance(Duration::from_millis(100));
589 assert_eq!(h.app().built.as_deref(), Some("sha256:4f2a"));
590 assert_eq!(h.app().tasks.entries()[0].outcome, Some(TaskOutcome::Done));
591 assert_eq!(h.screen(), "running 0\n");
592 }
593
594 #[test]
595 fn cancelling_wakes_the_sleep_and_drops_the_result() {
596 let mut h = Harness::new(Pipeline::default(), 20, 1);
597 h.send(Msg::Build).advance(Duration::from_millis(150)).send(Msg::Cancel);
598 let entry = &h.app().tasks.entries()[0];
599 assert_eq!(entry.outcome, Some(TaskOutcome::Cancelled));
600 assert_eq!(entry.fraction, Some(0.25));
601 assert!(h.app().built.is_none());
602 }
603
604 fn no_thread(_: String, _: Box<dyn FnOnce() + Send>) -> io::Result<()> {
605 Err(io::Error::other("no threads left"))
606 }
607
608 #[test]
609 fn a_task_whose_thread_cannot_start_fails_and_ends() {
610 let mut engine = Engine::new(Pipeline::default(), crate::env::Env::builtin(), TaskMode::Threads);
611 engine.spawner = no_thread;
612 engine.update(Msg::Build);
613 assert_eq!(engine.poll_tasks(), 2, "Finished, then Ended");
614 let entry = &engine.app.tasks.entries()[0];
615 assert_eq!(entry.outcome, Some(TaskOutcome::Failed("could not start a thread".into())));
616 assert_eq!((engine.app.tasks.running(), engine.pending_tasks), (0, 0));
617 assert!(engine.app.built.is_none());
618 }
619
620 struct Fragile;
622
623 impl App for Fragile {
624 type Msg = Option<()>;
625 fn update(&mut self, start: Option<()>) -> Command<Option<()>> {
626 if start.is_none() {
627 return Command::none();
628 }
629 Command::task(Task::new("Fragile", |_| Ok(None)).on_event(|event| match event {
630 TaskEvent::Finished { .. } => panic!("the message of the outcome failed"),
631 _ => None,
632 }))
633 }
634 fn view(&self, ui: &mut View<'_, Option<()>>) {
635 ui.add(Text::new("fragile"));
636 }
637 }
638
639 #[test]
640 fn a_task_whose_last_event_message_panics_still_ends() {
641 let mut engine = Engine::new(Fragile, crate::env::Env::builtin(), TaskMode::Threads);
642 engine.update(Some(()));
643 let started = Instant::now();
644 while engine.pending_tasks > 0 {
645 assert!(started.elapsed() < Duration::from_secs(10), "the runtime waits for the task forever");
646 engine.poll_tasks();
647 std::thread::sleep(Duration::from_millis(5));
648 }
649 }
650
651 #[test]
652 fn failures_and_panics_become_outcomes() {
653 let mut h = Harness::new(Pipeline::default(), 20, 1);
654 h.send(Msg::Fail).send(Msg::Panic);
655 assert_eq!(h.app().tasks.running(), 1, "the failing task still sleeps");
656 assert_eq!(h.app().tasks.entries()[1].outcome, Some(TaskOutcome::Failed("the task `Broken` panicked".into())));
657 h.advance(Duration::from_millis(50));
658 assert_eq!(h.app().tasks.entries()[0].outcome, Some(TaskOutcome::Failed("registry timed out".into())));
659 let mut tasks = h.app().tasks.clone();
660 tasks.clear_finished();
661 assert!(tasks.entries().is_empty());
662 }
663}