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 pub eval_def_hash: crate::types::DefinitionHash,
77 pub fingerprint: Option<crate::types::Fingerprint>,
79 pub fingerprints: BTreeMap<crate::config::ArtifactName, Option<crate::types::Fingerprint>>,
81 pub key: Option<crate::types::ReuseKey>,
83 #[serde(skip_serializing_if = "Option::is_none")]
84 pub last: Option<LastRequest>,
85 #[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
101struct 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#[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 keys: BTreeMap<&'c crate::config::EvalId, cache::Key>,
120}
121
122pub(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
130pub(crate) type Publish<'p> = &'p mut (dyn for<'a> FnMut(Progress<'a>) + Send);
132
133pub 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 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
149pub(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 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, ¶llelism, cancellation.clone()).await?
203 }
204 Some(publish) => {
205 scope
206 .progressively(targets, ¶llelism, cancellation.clone(), publish)
207 .await?
208 }
209 };
210 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
219struct 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 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 let preparation = cancellation.child_token();
255 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 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 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 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 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 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 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 =
436 cache::dependencies(self.config, evals[id])
437 .into_iter()
438 .all(|input| {
439 self.config.artifacts[input].fingerprint.is_none()
440 || fingerprints.contains_key(input)
441 });
442 let decided = keyed
443 && (matches!(cached.get(*id), Some(Claim::Reuse(_)))
444 || self
445 .graph
446 .gates(id)
447 .iter()
448 .all(|gate| settled.contains(gate)));
449 if decided {
450 settled.insert(*id);
451 } else {
452 checking.insert(*id);
453 }
454 }
455 }
456 }
457 checking
458 }
459}
460
461impl Counts {
462 fn action(&mut self, action: VerifyAction) {
463 match action {
464 VerifyAction::Execute => self.execute += 1,
465 VerifyAction::Reuse => self.reuse += 1,
466 VerifyAction::Wait => self.wait += 1,
467 VerifyAction::Blocked => self.blocked += 1,
468 }
469 }
470}