Skip to main content

flams_system/building/
queue.rs

1use crate::{backend::backend, FlamsExtension};
2
3use super::{
4    queue_manager::{QueueId, Semaphore},
5    BuildTask, BuildTaskId, Eta, QueueMessage, TaskRef, TaskState,
6};
7use flams_math_archives::{
8    artifacts::FileOrString,
9    backend::{AnyBackend, LocalBackend},
10    formats::{BuildResult, BuildTargetId, FormatOrTargets},
11    manager::ArchiveOrGroup,
12    source_files::{SourceEntry, SourceEntryRef},
13    utils::path_ext::RelPath,
14    Archive, BuildableArchive, LocallyBuilt, MathArchive,
15};
16use flams_utils::{
17    change_listener::{ChangeListener, ChangeSender},
18    prelude::HMap,
19    triomphe::Arc,
20};
21use ftml_ontology::utils::{time::Timestamp, RefTree};
22use ftml_uris::{ArchiveId, UriPath, UriWithArchive};
23use parking_lot::RwLock;
24use std::{collections::VecDeque, num::NonZeroU32};
25use tracing::{instrument, Instrument};
26
27#[derive(Debug)]
28pub(super) struct TaskMap {
29    pub(super) map: HMap<(ArchiveId, UriPath), BuildTask>,
30    pub(super) dependents: HMap<TaskRef, Vec<(BuildTask, BuildTargetId)>>,
31    pub(super) counter: NonZeroU32,
32    pub(super) total: usize,
33}
34
35impl Default for TaskMap {
36    fn default() -> Self {
37        Self {
38            map: HMap::default(),
39            dependents: HMap::default(),
40            counter: NonZeroU32::new(1).unwrap_or_else(|| unreachable!()),
41            total: 0,
42        }
43    }
44}
45
46#[derive(Debug)]
47pub enum QueueState {
48    Running(RunningQueue),
49    Idle,
50    Finished(FinishedQueue),
51}
52
53#[derive(Debug, Clone)]
54pub enum QueueName {
55    Global,
56    Sandbox { name: std::sync::Arc<str>, idx: u16 },
57}
58impl std::fmt::Display for QueueName {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        match self {
61            Self::Global => f.write_str("global"),
62            Self::Sandbox { name, idx } => {
63                f.write_str(name)?;
64                idx.fmt(f)
65            }
66        }
67    }
68}
69
70#[derive(Debug)]
71pub(super) struct QueueI {
72    backend: AnyBackend,
73    name: QueueName,
74    pub id: QueueId,
75    span: tracing::Span,
76    pub(super) map: RwLock<TaskMap>,
77    pub(super) sender: ChangeSender<QueueMessage>,
78    pub(super) state: RwLock<QueueState>,
79}
80
81#[derive(Debug, Clone)]
82pub struct Queue(pub(super) Arc<QueueI>);
83
84impl Queue {
85    pub(crate) fn new(id: QueueId, name: QueueName, backend: AnyBackend) -> Self {
86        Self(Arc::new(QueueI {
87            id,
88            name,
89            backend,
90            span: tracing::Span::current(),
91            map: RwLock::default(),
92            sender: ChangeSender::new(32),
93            state: RwLock::new(QueueState::Idle),
94        }))
95    }
96
97    #[inline]
98    #[must_use]
99    pub fn backend(&self) -> &AnyBackend {
100        &self.0.backend
101    }
102
103    #[must_use]
104    pub fn listener(&self) -> ChangeListener<QueueMessage> {
105        self.0.sender.listener()
106    }
107
108    #[instrument(level="info",parent=&self.0.span,skip_all,name="Collecting queue state")]
109    pub fn state_message(&self) -> QueueMessage {
110        match &*self.0.state.read() {
111            QueueState::Running(RunningQueue {
112                running,
113                queue,
114                blocked,
115                failed,
116                done,
117                ..
118            }) => QueueMessage::Started {
119                running: running.iter().map(BuildTask::as_message).collect(),
120                queue: queue.iter().map(BuildTask::as_message).collect(),
121                blocked: blocked.iter().map(BuildTask::as_message).collect(),
122                failed: failed.iter().map(BuildTask::as_message).collect(),
123                done: done.iter().map(BuildTask::as_message).collect(),
124            },
125            QueueState::Idle => QueueMessage::Idle(
126                self.0
127                    .map
128                    .read()
129                    .map
130                    .values()
131                    .map(BuildTask::as_message)
132                    .collect(),
133            ),
134            QueueState::Finished(FinishedQueue { done, failed }) => QueueMessage::Finished {
135                failed: failed.iter().map(BuildTask::as_message).collect(),
136                done: done.iter().map(BuildTask::as_message).collect(),
137            },
138        }
139    }
140
141    #[inline]
142    #[must_use]
143    pub fn name(&self) -> &QueueName {
144        &self.0.name
145    }
146
147    #[instrument(level = "info",
148    parent=&self.0.span,
149    target = "buildqueue",
150    name = "Running buildqueue",
151    skip_all
152  )]
153    pub fn start(&self, sem: Semaphore) {
154        let mut state = self.0.state.write();
155        if matches!(&*state, QueueState::Running(_)) {
156            return;
157        }
158        let map = self.0.map.read();
159        let mut running = RunningQueue::new(map.total);
160        tracing::info_span!("sorting...").in_scope(|| {
161            Self::sort(&map, &mut running);
162            tracing::info!("Done");
163        });
164        self.0.sender.lazy_send(|| QueueMessage::Started {
165            running: Vec::new(),
166            queue: running.queue.iter().map(BuildTask::as_message).collect(),
167            blocked: Vec::new(),
168            failed: Vec::new(),
169            done: Vec::new(),
170        });
171        *state = QueueState::Running(running);
172        drop(map);
173        drop(state);
174        match sem {
175            Semaphore::Linear => self.run_sync(),
176            #[cfg(feature = "tokio")]
177            Semaphore::Counting { inner: sem, .. } => {
178                tokio::task::spawn(self.clone().run_async(sem).in_current_span());
179            } //.in_current_span());}
180        }
181    }
182
183    #[inline]
184    fn run_sync(&self) {
185        while let Some((task, id)) = self.get_next() {
186            self.run_task(&task, id);
187        }
188        self.finish();
189    }
190
191    #[cfg(feature = "tokio")]
192    async fn run_async(self, sem: std::sync::Arc<tokio::sync::Semaphore>) {
193        loop {
194            let Ok(permit) = tokio::sync::Semaphore::acquire_owned(sem.clone()).await else {
195                break;
196            };
197            let Some((task, id)) = self.get_next_async().await else {
198                break;
199            };
200            let selfclone = self.clone();
201            let span = tracing::Span::current();
202            tokio::task::spawn_blocking(move || {
203                span.in_scope(move || selfclone.run_task_async(&task, id, permit));
204            });
205        }
206        loop {
207            if matches!(&*self.0.state.read(),QueueState::Running(RunningQueue{running,..}) if !running.is_empty())
208            {
209                tokio::time::sleep(std::time::Duration::from_millis(10)).await;
210            } else {
211                break;
212            }
213        }
214        self.finish();
215    }
216
217    fn finish(&self) {
218        let state = &mut *self.0.state.write();
219        let QueueState::Running(RunningQueue { done, failed, .. }) = state else {
220            unreachable!()
221        };
222        let done = std::mem::take(done);
223        let failed = std::mem::take(failed);
224        self.0.sender.lazy_send(|| QueueMessage::Finished {
225            failed: failed.iter().map(BuildTask::as_message).collect(),
226            done: done.iter().map(BuildTask::as_message).collect(),
227        });
228        *state = QueueState::Finished(FinishedQueue { done, failed });
229    }
230
231    #[instrument(level="info",parent=&self.0.span,skip_all,name="Requeueing failed")]
232    pub fn requeue_failed(&self) {
233        let mut state = self.0.state.write();
234        let QueueState::Finished(FinishedQueue { failed, .. }) = &mut *state else {
235            return;
236        };
237        let failed = std::mem::take(failed);
238        *state = QueueState::Idle;
239        drop(state);
240        if failed.is_empty() {
241            return;
242        }
243        let map = &mut *self.0.map.write();
244        map.dependents.clear();
245        map.counter = unsafe { NonZeroU32::new_unchecked(1) };
246        map.total = failed.iter().map(|t| t.0.steps.len()).sum();
247        map.map.clear();
248        for t in failed {
249            for s in &t.0.steps {
250                s.0.state.set(TaskState::None);
251            }
252            map.map.insert(
253                (t.archive().archive_id().clone(), t.0.rel_path.clone()),
254                BuildTask(Arc::new(super::BuildTaskI {
255                    id: BuildTaskId(map.counter),
256                    rel_path: t.0.rel_path.clone(),
257                    uri: t.0.uri.clone(),
258                    steps: t.0.steps.clone(),
259                    source: t.0.source.clone(),
260                })),
261            );
262            map.counter = map.counter.saturating_add(1);
263        }
264        self.0.sender.lazy_send(|| {
265            QueueMessage::Idle(map.map.values().map(BuildTask::as_message).collect())
266        });
267    }
268
269    #[cfg(feature = "tokio")]
270    #[inline]
271    fn run_task_async(
272        &self,
273        task: &BuildTask,
274        target: BuildTargetId,
275        permit: tokio::sync::OwnedSemaphorePermit,
276    ) {
277        self.run_task(task, target);
278        drop(permit);
279    }
280
281    #[allow(clippy::cast_possible_truncation)]
282    #[allow(clippy::significant_drop_tightening)]
283    fn run_task(&self, task: &BuildTask, target: BuildTargetId) {
284        self.0.sender.lazy_send(|| QueueMessage::TaskStarted {
285            id: task.0.id,
286            target,
287        });
288        let spec = task.as_build_spec(&self.0.backend);
289        //println!("Running task {target}");
290        let BuildResult { log, result } = tracing::info_span!(target:"buildqueue","Running task",
291          archive = %task.0.uri.archive_id(),
292          rel_path = %task.0.rel_path,
293          format = %target
294        )
295        .in_scope(|| (target.run)(spec));
296        //println!("Finished running task {target}");
297        /*let (idx, _) = task
298        .steps()
299        .iter()
300        .enumerate()
301        .find(|(_, s)| s.0.target == target)
302        .unwrap_or_else(|| unreachable!());*/
303        let mut lock = self.0.state.write();
304        let QueueState::Running(ref mut state) = &mut *lock else {
305            unreachable!()
306        };
307        state.running.retain(|t| t != task);
308        let eta = state.timer.update(1);
309
310        let (mut err, res) = match result {
311            Ok(res) => (None, res),
312            Err(e) => (Some(e), None),
313        };
314
315        if let Err(e) =
316            self.0
317                .backend
318                .save(task.document_uri(), Some(task.rel_path()), log, target, res)
319        {
320            tracing::error!("Error saving build result: {e}");
321            if err.is_none() {
322                err = Some(Vec::new());
323            }
324        }
325
326        match err {
327            None => {
328                let mut found = false;
329                let mut requeue = false;
330                for s in task.steps() {
331                    if s.0.target == target {
332                        found = true;
333                        s.0.state.set(TaskState::Done);
334                    } else if found {
335                        s.0.state.set(TaskState::Queued);
336                        requeue = true;
337                        break;
338                    }
339                }
340                if requeue {
341                    state.queue.push_front(task.clone());
342                } else {
343                    state.done.push(task.clone());
344                }
345                drop(lock);
346
347                self.0.sender.lazy_send(|| QueueMessage::TaskSuccess {
348                    id: task.0.id,
349                    target,
350                    eta,
351                });
352            }
353            Some(deps) => {
354                /*
355                let mut block = false;
356                for d in deps {
357                    match d {
358                        flams_math_archives::formats::TaskDependency::Physical { task, strict } => {
359                            if state.running.iter().chain(state.blocked.iter()).any(|t| task.archive == t.archive().id && task.rel_path == *t.rel_path() && t.get_step(task.target).is_some()) {
360                                block = true;
361                            }
362                        }
363                        flams_math_archives::formats::TaskDependency::Logical { uri, strict } => {
364                            self.backend().with_local_archive(uri.archive_id(), |a| if let Some(a) = a {
365                                a.do
366                            })
367                        }
368
369                    }
370                } */
371                let mut found = false;
372                if deps.is_empty() || state.queue.is_empty() {
373                    for s in task.steps() {
374                        if s.0.target == target {
375                            found = true;
376                        }
377                        if found {
378                            s.0.state.set(TaskState::Failed);
379                        }
380                    }
381                    state.failed.push(task.clone());
382                    self.0.sender.lazy_send(|| QueueMessage::TaskFailed {
383                        id: task.0.id,
384                        target,
385                        eta,
386                    });
387                } else {
388                    // TODO: handle dependencies
389                    let mut found = false;
390                    for s in task.steps() {
391                        if s.0.target == target {
392                            found = true;
393                        }
394                        if found {
395                            s.0.state.set(TaskState::Blocked);
396                        }
397                    }
398                    state.blocked.push(task.clone());
399                    self.0.sender.lazy_send(|| QueueMessage::TaskBlocked {
400                        id: task.0.id,
401                        target,
402                        eta,
403                    });
404                }
405                drop(lock);
406            }
407        }
408    }
409
410    fn maybe_restart(&self) {
411        let mut state = self.0.state.write();
412        if let QueueState::Finished(_) = &mut *state {
413            drop(state);
414            self.requeue_failed();
415        }
416    }
417
418    #[instrument(level = "info",
419    parent=&self.0.span,
420    target = "buildqueue",
421    name = "Queueing tasks",
422    skip_all
423  )]
424    #[deprecated(note = "needs refactoring: archive need not be local, etc.")]
425    pub fn enqueue_all(&self, target: FormatOrTargets<'_>, stale_only: bool, clean: bool) -> usize {
426        self.maybe_restart();
427        if let AnyBackend::Sandbox(b) = &self.0.backend {
428            b.clear();
429            backend().with_archives(|archives| {
430                for a in archives {
431                    let Archive::Local(archive) = a else { continue };
432                    let matches = archive.is_meta()
433                        || (match target {
434                            FormatOrTargets::Format(f) => archive.formats().contains(&f),
435                            FormatOrTargets::Targets(targets) => archive
436                                .formats()
437                                .iter()
438                                .flat_map(|fmt| fmt.targets)
439                                .any(|t| targets.contains(t)),
440                        });
441                    if matches {
442                        b.maybe_copy(archive);
443                        if clean {
444                            let _ =
445                                std::fs::remove_dir_all(b.path_for(archive.id()).join(".flams"));
446                        }
447                    }
448                }
449                b.load_all();
450            });
451        }
452        let mut acc = 0;
453        self.0.backend.with_archives(|archives| {
454            for a in archives {
455                let Archive::Local(archive) = a else { continue };
456                acc += archive.with_sources(|d| {
457                    let d = d.dfs();
458                    let map = &mut *self.0.map.write();
459                    Self::enqueue(
460                        map,
461                        &self.0.backend,
462                        a,
463                        target,
464                        stale_only,
465                        d.filter_map(|e| match e {
466                            SourceEntry::Dir(_) => None,
467                            SourceEntry::File(f) => Some(f),
468                        }),
469                    )
470                });
471            }
472        });
473        acc
474    }
475
476    #[instrument(level = "debug",
477    parent=&self.0.span,
478    target = "buildqueue",
479    name = "Queueing tasks",
480    skip_all
481  )]
482    #[deprecated(note = "needs refactoring: assumes LocalArchive everywhere")]
483    pub fn enqueue_group(
484        &self,
485        id: &ArchiveId,
486        target: FormatOrTargets,
487        stale_only: bool,
488        clean: bool,
489    ) -> usize {
490        self.maybe_restart();
491        if let AnyBackend::Sandbox(b) = &self.0.backend {
492            b.require(id, false);
493        }
494        self.0.backend.with_archive_or_group(id, |g| match g {
495            None => 0,
496            Some(ArchiveOrGroup::Archive(id)) => self.0.backend.with_archive(id, |a| {
497                let Some(archive) = a else { return 0 };
498                if let Archive::Local(a) = archive {
499                    let matches = match target {
500                        FormatOrTargets::Format(f) => a.formats().contains(&f),
501                        FormatOrTargets::Targets(targets) => a
502                            .formats()
503                            .iter()
504                            .flat_map(|fmt| fmt.targets)
505                            .any(|t| targets.contains(t)),
506                    };
507                    if matches {
508                        if clean {
509                            if let AnyBackend::Sandbox(b) = &self.0.backend {
510                                let _ = std::fs::remove_dir_all(
511                                    b.path_for(archive.id()).join(".flams"),
512                                );
513                            } else if let Archive::Local(a) = archive {
514                                let _ = std::fs::remove_dir_all(a.out_dir());
515                            }
516                        }
517                        a.with_sources(|d| {
518                            let map = &mut *self.0.map.write();
519                            Self::enqueue(
520                                map,
521                                &self.0.backend,
522                                archive,
523                                target,
524                                stale_only,
525                                d.dfs().filter_map(|e| match e {
526                                    SourceEntry::Dir(_) => None,
527                                    SourceEntry::File(f) => Some(f),
528                                }),
529                            )
530                        })
531                    } else {
532                        0
533                    }
534                } else {
535                    0
536                }
537            }),
538            Some(ArchiveOrGroup::Group(g)) => {
539                let map = &mut *self.0.map.write();
540                let mut ret = 0;
541                for id in g.dfs().filter_map(|e| match e {
542                    ArchiveOrGroup::Archive(id) => Some(id),
543                    ArchiveOrGroup::Group(_) => None,
544                }) {
545                    ret += self.0.backend.with_archive(id, |a| {
546                        let Some(archive) = a else { return 0 };
547                        if let Archive::Local(a) = archive {
548                            let matches = match target {
549                                FormatOrTargets::Format(f) => a.formats().contains(&f),
550                                FormatOrTargets::Targets(targets) => a
551                                    .formats()
552                                    .iter()
553                                    .flat_map(|fmt| fmt.targets)
554                                    .any(|t| targets.contains(t)),
555                            };
556                            if matches {
557                                if clean {
558                                    if let AnyBackend::Sandbox(b) = &self.0.backend {
559                                        let _ = std::fs::remove_dir_all(
560                                            b.path_for(archive.id()).join(".flams"),
561                                        );
562                                    } else if let Archive::Local(a) = archive {
563                                        let _ = std::fs::remove_dir_all(a.out_dir());
564                                    }
565                                }
566                                a.with_sources(|d| {
567                                    Self::enqueue(
568                                        map,
569                                        &self.0.backend,
570                                        archive,
571                                        target,
572                                        stale_only,
573                                        d.dfs().filter_map(|e| match e {
574                                            SourceEntry::Dir(_) => None,
575                                            SourceEntry::File(f) => Some(f),
576                                        }),
577                                    )
578                                })
579                            } else {
580                                0
581                            }
582                        } else {
583                            0
584                        }
585                    });
586                }
587                ret
588            }
589        })
590    }
591
592    #[instrument(level = "info",
593    parent=&self.0.span,
594    target = "buildqueue",
595    name = "Queueing tasks",
596    skip_all
597  )]
598    #[deprecated(note = "needs refactoring: assumes LocalArchive everywhere")]
599    pub fn enqueue_archive(
600        &self,
601        id: &ArchiveId,
602        target: FormatOrTargets,
603        stale_only: bool,
604        rel_path: Option<RelPath<'_>>,
605        clean: bool,
606    ) -> usize {
607        self.maybe_restart();
608        if let AnyBackend::Sandbox(b) = &self.0.backend {
609            b.require(id, true);
610        }
611        self.0.backend.with_archive(id, |archive| {
612            let Some(archive) = archive else { return 0 };
613            if clean {
614                if let AnyBackend::Sandbox(b) = &self.0.backend {
615                    let _ = std::fs::remove_dir_all(b.path_for(archive.id()).join(".flams"));
616                } else if let Archive::Local(a) = archive {
617                    let _ = std::fs::remove_dir_all(a.out_dir());
618                }
619            }
620            if let Archive::Local(a) = archive {
621                a.with_sources(|d| match rel_path {
622                    None => {
623                        let map = &mut *self.0.map.write();
624                        Self::enqueue(
625                            map,
626                            &self.0.backend,
627                            archive,
628                            target,
629                            stale_only,
630                            d.dfs().filter_map(|e| match e {
631                                SourceEntry::Dir(_) => None,
632                                SourceEntry::File(f) => Some(f),
633                            }),
634                        )
635                    }
636                    Some(p) => {
637                        let Some(d) = d.find(p) else { return 0 };
638                        match d {
639                            SourceEntryRef::Dir(d) => {
640                                let map = &mut *self.0.map.write();
641                                Self::enqueue(
642                                    map,
643                                    &self.0.backend,
644                                    archive,
645                                    target,
646                                    stale_only,
647                                    d.dfs().filter_map(|e| match e {
648                                        SourceEntry::Dir(_) => None,
649                                        SourceEntry::File(f) => Some(f),
650                                    }),
651                                )
652                            }
653                            SourceEntryRef::File(f) => {
654                                let map = &mut *self.0.map.write();
655                                Self::enqueue(
656                                    map,
657                                    &self.0.backend,
658                                    archive,
659                                    target,
660                                    stale_only,
661                                    std::iter::once(f),
662                                )
663                            }
664                        }
665                    }
666                })
667            } else {
668                0
669            }
670        })
671    }
672}
673
674#[derive(Debug)]
675pub struct RunningQueue {
676    pub(super) queue: VecDeque<BuildTask>,
677    pub(super) blocked: Vec<BuildTask>,
678    pub(super) done: Vec<BuildTask>,
679    pub(super) failed: Vec<BuildTask>,
680    pub(super) running: Vec<BuildTask>,
681    timer: Timer,
682}
683impl RunningQueue {
684    fn new(total: usize) -> Self {
685        Self {
686            queue: VecDeque::new(),
687            failed: Vec::new(),
688            blocked: Vec::new(),
689            done: Vec::new(),
690            running: Vec::new(),
691            timer: Timer::new(total),
692        }
693    }
694}
695
696#[derive(Debug)]
697pub struct FinishedQueue {
698    pub(super) done: Vec<BuildTask>,
699    pub(super) failed: Vec<BuildTask>,
700}
701
702#[derive(Debug)]
703struct Timer {
704    started: Timestamp,
705    steps: usize,
706    done: usize,
707}
708impl Timer {
709    fn new(total: usize) -> Self {
710        Self {
711            started: Timestamp::now(),
712            steps: total,
713            done: 0,
714        }
715    }
716    #[allow(clippy::cast_precision_loss)]
717    fn update(&mut self, dones: u8) -> Eta {
718        self.done += dones as usize;
719        let avg = self.started.since_now() * (1.0 / (self.done as f64));
720        let time_left = avg * ((self.steps - self.done) as f64);
721        Eta {
722            time_left,
723            done: self.done,
724            total: self.steps,
725        }
726    }
727}