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));
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 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 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#[derive(Debug, Clone, PartialEq)]
418pub struct TaskEntry {
419 pub id: TaskId,
421 pub label: String,
423 pub fraction: Option<f32>,
425 pub note: Option<String>,
427 pub outcome: Option<TaskOutcome>,
429}
430
431#[derive(Debug, Clone, Default, PartialEq)]
433pub struct Tasks {
434 entries: Vec<TaskEntry>,
435}
436
437impl Tasks {
438 #[must_use]
440 pub fn new() -> Self {
441 Self::default()
442 }
443
444 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 #[must_use]
475 pub fn entries(&self) -> &[TaskEntry] {
476 &self.entries
477 }
478
479 #[must_use]
481 pub fn get(&self, id: TaskId) -> Option<&TaskEntry> {
482 self.entries.iter().find(|entry| entry.id == id)
483 }
484
485 #[must_use]
487 pub fn running(&self) -> usize {
488 self.entries.iter().filter(|entry| entry.outcome.is_none()).count()
489 }
490
491 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 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}