Skip to main content

artifactize_core/project/
status.rs

1mod states;
2mod view;
3pub use states::{ArtifactCondition, EvalCondition, VerifyAction};
4#[cfg(test)]
5mod tests;
6
7use std::{
8    collections::{BTreeMap, BTreeSet},
9    path::Path,
10};
11
12use serde::Serialize;
13use tokio_util::sync::CancellationToken;
14
15use crate::{
16    cache,
17    config::{Profile, read_workspace_config},
18    graph::{Evidence, Graph, Readiness},
19    project::{VerifyOptions, selection::select_profiles},
20    remote::Session,
21    store::{self, Claim, LastRequest},
22};
23
24use super::selection::Selection;
25
26#[derive(Debug, Serialize)]
27#[serde(rename_all = "camelCase")]
28pub struct StatusView {
29    pub selection: Selection,
30    pub recursive: bool,
31    pub force: bool,
32    pub selected_eval_ids: Vec<crate::config::EvalId>,
33    pub included_eval_ids: Vec<crate::config::EvalId>,
34    pub satisfied: bool,
35    pub obligations: Vec<crate::config::ArtifactName>,
36    pub artifacts: Vec<ArtifactState>,
37    pub evals: Vec<EvalState>,
38    pub counts: Counts,
39    #[serde(skip_serializing_if = "Vec::is_empty")]
40    pub unreadable: Vec<store::Unreadable>,
41}
42
43#[derive(Debug, Serialize)]
44#[serde(rename_all = "camelCase")]
45pub struct ArtifactState {
46    pub id: crate::config::ArtifactName,
47    pub kind: crate::config::ArtifactKind,
48    #[serde(with = "crate::platform::path_serde")]
49    pub path: std::path::PathBuf,
50    pub tags: Vec<String>,
51    pub state: ArtifactCondition,
52    pub reason: String,
53    pub eval_ids: Vec<crate::config::EvalId>,
54    pub passed: usize,
55    pub total: usize,
56    pub satisfied: bool,
57    pub obligations: Vec<crate::config::ArtifactName>,
58}
59
60#[derive(Debug, Serialize)]
61#[serde(rename_all = "camelCase")]
62pub struct EvalState {
63    pub id: crate::config::EvalId,
64    pub target: crate::config::ArtifactName,
65    pub title: String,
66    pub profile: Profile,
67    pub selected: bool,
68    pub included: bool,
69    pub force: bool,
70    pub state: EvalCondition,
71    pub action: VerifyAction,
72    pub reason: String,
73    pub blocked_by: Vec<crate::store::Blocker>,
74    pub obligations: Vec<crate::config::ArtifactName>,
75    /// The Eval definition hash: the evaluation criteria the key covers.
76    pub eval_def_hash: crate::types::DefinitionHash,
77    /// The target's current fingerprint; null without one.
78    pub fingerprint: Option<crate::types::Fingerprint>,
79    /// Each Artifact the eval depends on, with its current fingerprint or null without one.
80    pub fingerprints: BTreeMap<crate::config::ArtifactName, Option<crate::types::Fingerprint>>,
81    /// The reuse key composed of the hash and the fingerprints; null when one is missing.
82    pub key: Option<crate::types::ReuseKey>,
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub last: Option<LastRequest>,
85    /// Why the key no longer matches the newest cached result for this Eval definition: the
86    /// target's changed files and the dependency Artifacts whose fingerprints changed.
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub changes: Option<cache::Changes>,
89}
90
91#[derive(Debug, Default, Serialize)]
92pub struct Counts {
93    pub artifacts: BTreeMap<ArtifactCondition, usize>,
94    pub evals: BTreeMap<EvalCondition, usize>,
95    pub execute: usize,
96    pub reuse: usize,
97    pub wait: usize,
98    pub blocked: usize,
99}
100
101/// All effects needed by the status projection have finished before this is assembled.
102struct Snapshot<'a> {
103    required: &'a BTreeSet<&'a crate::config::ArtifactName>,
104    selected_eval_ids: &'a [crate::config::EvalId],
105    included_eval_ids: &'a [crate::config::EvalId],
106    fingerprints: &'a BTreeMap<&'a crate::config::ArtifactName, cache::PreparedFingerprint>,
107    known: &'a Known<'a>,
108}
109
110/// What the store and the remote review store say about the keys a scan computed so far.
111#[derive(Default)]
112struct Known<'c> {
113    cached: BTreeMap<crate::config::EvalId, Claim>,
114    previous: BTreeMap<(crate::config::EvalId, crate::types::DefinitionHash), store::Execution>,
115    latest: BTreeMap<crate::config::EvalId, LastRequest>,
116    waiting: Vec<store::RequestView>,
117    unreadable: Vec<store::Unreadable>,
118    /// Every key computed so far, by eval.
119    keys: BTreeMap<&'c crate::config::EvalId, cache::Key>,
120}
121
122/// What a scan knows part way: the status as far as its fingerprints go, the results it found
123/// to reuse, and the evals whose action may still change.
124pub(crate) struct Progress<'a> {
125    pub view: StatusView,
126    pub cached: &'a BTreeMap<crate::config::EvalId, Claim>,
127    pub checking: BTreeSet<&'a crate::config::EvalId>,
128}
129
130/// Where a scan hands what it knows part way.
131pub(crate) type Publish<'p> = &'p mut (dyn for<'a> FnMut(Progress<'a>) + Send);
132
133/// Prepare current fingerprints without executing evals or changing saved evidence.
134pub async fn status(
135    repo: &Path,
136    state_dir: Option<&Path>,
137    selection: &Selection,
138    options: &VerifyOptions,
139    cancellation: CancellationToken,
140) -> Result<StatusView, String> {
141    // Options are checked before anything is read.
142    super::fingerprint_parallelism(options)?;
143    let config = read_workspace_config(repo).map_err(|error| error.to_string())?;
144    scan(config, state_dir, selection, options, cancellation, None)
145        .await
146        .map(|(view, _)| view)
147}
148
149/// [`status`] of a workspace configuration already read, with the claims it found by eval.
150/// With `publish`, what it knows is handed there as its fingerprints come in.
151pub(crate) async fn scan(
152    config: crate::config::RepoConfig,
153    state_dir: Option<&Path>,
154    selection: &Selection,
155    options: &VerifyOptions,
156    cancellation: CancellationToken,
157    publish: Option<Publish<'_>>,
158) -> Result<(StatusView, BTreeMap<crate::config::EvalId, Claim>), String> {
159    let parallelism = super::fingerprint_parallelism(options)?;
160    let config = select_profiles(
161        config,
162        selection,
163        options.profile.as_ref(),
164        options.recursive,
165    )?;
166    let graph = Graph::new(&config).map_err(|error| error.to_string())?;
167    let selected = selection.resolve(&config)?;
168    let required: BTreeSet<_> = graph
169        .dependency_closure(&selected.roots)
170        .map_err(|error| error.to_string())?
171        .into_iter()
172        .collect();
173    let selected_eval_ids: Vec<_> = selected.evals.iter().map(|eval| eval.id.clone()).collect();
174    let included_eval_ids: Vec<_> = selection
175        .included_evals(&config, options.recursive)?
176        .iter()
177        .map(|eval| eval.id.clone())
178        .collect();
179    let state = store::state_dir(state_dir)?;
180    // Read-only: a store record that completed after the local latest counts as reuse without
181    // being mirrored, as verify would take it before claiming. --force reads nothing from the
182    // store, so its prediction has none.
183    let remote = if options.force {
184        None
185    } else {
186        Session::open(Some(&state), Some(&config.root))?
187    };
188    let scope = Scope {
189        config: &config,
190        graph: &graph,
191        selection,
192        options,
193        required: &required,
194        selected_eval_ids: &selected_eval_ids,
195        included_eval_ids: &included_eval_ids,
196        state: &state,
197        remote: remote.as_deref(),
198    };
199    let targets = cache::fingerprint_targets(&config, &required);
200    let fingerprints = match publish {
201        None => {
202            cache::prepare(&config, targets, &state, &parallelism, cancellation.clone()).await?
203        }
204        Some(publish) => {
205            scope
206                .progressively(targets, &parallelism, cancellation.clone(), publish)
207                .await?
208        }
209    };
210    // Read once every fingerprint is in, so a Run that ended meanwhile counts.
211    let known = scope.read(&fingerprints).await?;
212    if cancellation.is_cancelled() {
213        return Err("Project preparation was cancelled.".into());
214    }
215    let view = scope.assemble(&fingerprints, &known)?;
216    Ok((view, known.cached))
217}
218
219/// What one scan reads against.
220struct Scope<'a> {
221    config: &'a crate::config::RepoConfig,
222    graph: &'a Graph<'a>,
223    selection: &'a Selection,
224    options: &'a VerifyOptions,
225    required: &'a BTreeSet<&'a crate::config::ArtifactName>,
226    selected_eval_ids: &'a [crate::config::EvalId],
227    included_eval_ids: &'a [crate::config::EvalId],
228    state: &'a Path,
229    remote: Option<&'a Session>,
230}
231
232type Fingerprints<'a> = BTreeMap<&'a crate::config::ArtifactName, cache::PreparedFingerprint>;
233
234impl<'a> Scope<'a> {
235    /// Prepare the fingerprints, handing `publish` what is known each time some come in: first
236    /// the graph with every eval being checked, then each action as soon as it cannot change.
237    async fn progressively(
238        &self,
239        targets: BTreeSet<&'a crate::config::ArtifactName>,
240        parallelism: &cache::Parallelism,
241        cancellation: CancellationToken,
242        publish: Publish<'_>,
243    ) -> Result<Fingerprints<'a>, String> {
244        let mut fingerprints = Fingerprints::new();
245        let first = Known::default();
246        publish(Progress {
247            view: self.assemble(&fingerprints, &first)?,
248            cached: &first.cached,
249            checking: self.checking(&fingerprints, &first.cached),
250        });
251        let mut known = self.read(&fingerprints).await?;
252        let (ready, mut arrived) = tokio::sync::mpsc::unbounded_channel();
253        // A failed store read stops the fingerprints nobody would read any more.
254        let preparation = cancellation.child_token();
255        // Dependency first: an eval's action is known once its gates' are, so what is upstream
256        // is fingerprinted first and the graph fills in from there.
257        let targets = self
258            .graph
259            .components()
260            .iter()
261            .flat_map(|component| &component.artifacts)
262            .filter_map(|id| targets.get(*id).copied());
263        let preparing = cache::prepare_each(
264            self.config,
265            targets,
266            self.state,
267            parallelism,
268            preparation.clone(),
269            move |id, fingerprint| {
270                let _ = ready.send((id, fingerprint));
271            },
272        );
273        let publishing = async {
274            while let Some((id, fingerprint)) = arrived.recv().await {
275                let started = tokio::time::Instant::now();
276                fingerprints.insert(id, fingerprint);
277                while let Ok((id, fingerprint)) = arrived.try_recv() {
278                    fingerprints.insert(id, fingerprint);
279                }
280                self.absorb(&mut known, &fingerprints).await?;
281                publish(Progress {
282                    view: self.assemble(&fingerprints, &known)?,
283                    cached: &known.cached,
284                    checking: self.checking(&fingerprints, &known.cached),
285                });
286                // Publishing takes from the fingerprints' time: leave them as long again.
287                tokio::time::sleep(started.elapsed()).await;
288            }
289            Ok::<_, String>(())
290        };
291        let publishing = async {
292            let published = publishing.await;
293            if published.is_err() {
294                preparation.cancel();
295            }
296            published
297        };
298        let (prepared, published) = tokio::join!(preparing, publishing);
299        prepared?;
300        published?;
301        Ok(fingerprints)
302    }
303
304    /// What the store says about `fingerprints`, read afresh.
305    async fn read(&self, fingerprints: &Fingerprints<'a>) -> Result<Known<'a>, String> {
306        let mut known = Known {
307            latest: store::read_latest_requests(self.state, &self.config.root).await?,
308            waiting: store::read_waiting(self.state, Some(&self.config.root)).await?,
309            unreadable: store::read_unreadable(self.state, Some(&self.config.root)).await?,
310            ..Known::default()
311        };
312        self.absorb(&mut known, fingerprints).await?;
313        Ok(known)
314    }
315
316    /// Look up the keys `fingerprints` complete that `known` has not looked up yet.
317    async fn absorb(
318        &self,
319        known: &mut Known<'a>,
320        fingerprints: &Fingerprints<'a>,
321    ) -> Result<(), String> {
322        let new: BTreeMap<&'a crate::config::EvalId, cache::Key> = self
323            .config
324            .evals
325            .iter()
326            .filter(|eval| !known.keys.contains_key(&eval.id))
327            .filter_map(|eval| {
328                Some((
329                    &eval.id,
330                    cache::eval_key(self.config, eval, fingerprints).ok()?,
331                ))
332            })
333            .collect();
334        let selected: BTreeSet<_> = self.selected_eval_ids.iter().collect();
335        let keys: BTreeMap<_, _> = new
336            .iter()
337            .filter(|(id, _)| !(self.options.force && selected.contains(**id)))
338            .map(|(id, key)| (*id, key))
339            .collect();
340        let mut cached = store::read_profiled_executions(
341            self.state,
342            self.config
343                .evals
344                .iter()
345                .filter_map(|eval| {
346                    keys.get(&eval.id).map(|key| {
347                        (
348                            eval.id.clone(),
349                            key.value.clone(),
350                            eval.declaration.profile().into(),
351                        )
352                    })
353                })
354                .collect(),
355        )
356        .await?;
357        if let Some(remote) = self.remote {
358            let all: Vec<_> = keys
359                .values()
360                .map(|key| key.value.clone())
361                .collect::<BTreeSet<_>>()
362                .into_iter()
363                .collect();
364            for execution in remote.lookup(&all).await? {
365                let key = execution.key.clone().expect("remote key");
366                for (id, _) in keys.iter().filter(|(_, candidate)| candidate.value == key) {
367                    let newer = match cached.get(*id) {
368                        Some(Claim::Reuse(local)) => execution.completed_after(local),
369                        _ => true,
370                    };
371                    if newer {
372                        cached.insert((*id).clone(), Claim::Reuse(Box::new(execution.clone())));
373                    }
374                }
375            }
376        }
377        // Explain changed keys against the newest cached result for the same Eval definition.
378        let stale: Vec<_> = keys
379            .iter()
380            .filter(|(id, _)| !matches!(cached.get(**id), Some(Claim::Reuse(_))))
381            .map(|(id, key)| ((*id).clone(), key.eval_def_hash.clone()))
382            .collect();
383        known
384            .previous
385            .extend(store::read_latest_cached(self.state, &stale).await?);
386        known.cached.extend(cached);
387        known.keys.extend(new);
388        Ok(())
389    }
390
391    fn assemble(
392        &self,
393        fingerprints: &Fingerprints<'a>,
394        known: &Known<'a>,
395    ) -> Result<StatusView, String> {
396        view::assemble(
397            self.config,
398            self.graph,
399            self.selection,
400            self.options,
401            Snapshot {
402                required: self.required,
403                selected_eval_ids: self.selected_eval_ids,
404                included_eval_ids: self.included_eval_ids,
405                fingerprints,
406                known,
407            },
408        )
409    }
410
411    /// The evals whose action may still change: a fingerprint their key needs is not in yet,
412    /// or, unless they reuse a result, the action of an eval they wait for may still change.
413    fn checking(
414        &self,
415        fingerprints: &Fingerprints<'a>,
416        cached: &BTreeMap<crate::config::EvalId, Claim>,
417    ) -> BTreeSet<&'a crate::config::EvalId> {
418        let evals: BTreeMap<_, _> = self
419            .config
420            .evals
421            .iter()
422            .map(|eval| (&eval.id, eval))
423            .collect();
424        let mut settled = BTreeSet::new();
425        let mut checking = BTreeSet::new();
426        // Dependency first, so an eval's gates are decided before it; gates in its own cycle
427        // are not, and the cycle stays being checked until every fingerprint is in.
428        for component in self.graph.components() {
429            for artifact in component
430                .artifacts
431                .iter()
432                .filter(|id| self.required.contains(*id))
433            {
434                for id in &self.graph.artifacts()[*artifact].evals {
435                    let keyed = cache::dependencies(evals[id]).into_iter().all(|input| {
436                        self.config.artifacts[input].fingerprint.is_none()
437                            || fingerprints.contains_key(input)
438                    });
439                    let decided = keyed
440                        && (matches!(cached.get(*id), Some(Claim::Reuse(_)))
441                            || self
442                                .graph
443                                .gates(id)
444                                .iter()
445                                .all(|gate| settled.contains(gate)));
446                    if decided {
447                        settled.insert(*id);
448                    } else {
449                        checking.insert(*id);
450                    }
451                }
452            }
453        }
454        checking
455    }
456}
457
458impl Counts {
459    fn action(&mut self, action: VerifyAction) {
460        match action {
461            VerifyAction::Execute => self.execute += 1,
462            VerifyAction::Reuse => self.reuse += 1,
463            VerifyAction::Wait => self.wait += 1,
464            VerifyAction::Blocked => self.blocked += 1,
465        }
466    }
467}