mod states;
mod view;
pub use states::{ArtifactCondition, EvalCondition, VerifyAction};
#[cfg(test)]
mod tests;
use std::{
collections::{BTreeMap, BTreeSet},
path::Path,
};
use serde::Serialize;
use tokio_util::sync::CancellationToken;
use crate::{
cache,
config::{Profile, read_workspace_config},
graph::{Evidence, Graph, Readiness},
project::{VerifyOptions, selection::select_profiles},
remote::Session,
store::{self, Claim, LastRequest},
};
use super::selection::Selection;
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StatusView {
pub selection: Selection,
pub recursive: bool,
pub force: bool,
pub selected_eval_ids: Vec<crate::config::EvalId>,
pub included_eval_ids: Vec<crate::config::EvalId>,
pub satisfied: bool,
pub obligations: Vec<crate::config::ArtifactName>,
pub artifacts: Vec<ArtifactState>,
pub evals: Vec<EvalState>,
pub counts: Counts,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub unreadable: Vec<store::Unreadable>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ArtifactState {
pub id: crate::config::ArtifactName,
pub kind: crate::config::ArtifactKind,
#[serde(with = "crate::platform::path_serde")]
pub path: std::path::PathBuf,
pub tags: Vec<String>,
pub state: ArtifactCondition,
pub reason: String,
pub eval_ids: Vec<crate::config::EvalId>,
pub passed: usize,
pub total: usize,
pub satisfied: bool,
pub obligations: Vec<crate::config::ArtifactName>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EvalState {
pub id: crate::config::EvalId,
pub target: crate::config::ArtifactName,
pub title: String,
pub profile: Profile,
pub selected: bool,
pub included: bool,
pub force: bool,
pub state: EvalCondition,
pub action: VerifyAction,
pub reason: String,
pub blocked_by: Vec<crate::store::Blocker>,
pub obligations: Vec<crate::config::ArtifactName>,
pub eval_def_hash: crate::types::DefinitionHash,
pub fingerprint: Option<crate::types::Fingerprint>,
pub fingerprints: BTreeMap<crate::config::ArtifactName, Option<crate::types::Fingerprint>>,
pub key: Option<crate::types::ReuseKey>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last: Option<LastRequest>,
#[serde(skip_serializing_if = "Option::is_none")]
pub changes: Option<cache::Changes>,
}
#[derive(Debug, Default, Serialize)]
pub struct Counts {
pub artifacts: BTreeMap<ArtifactCondition, usize>,
pub evals: BTreeMap<EvalCondition, usize>,
pub execute: usize,
pub reuse: usize,
pub wait: usize,
pub blocked: usize,
}
struct Snapshot<'a> {
required: &'a BTreeSet<&'a crate::config::ArtifactName>,
selected_eval_ids: &'a [crate::config::EvalId],
included_eval_ids: &'a [crate::config::EvalId],
fingerprints: &'a BTreeMap<&'a crate::config::ArtifactName, cache::PreparedFingerprint>,
known: &'a Known<'a>,
}
#[derive(Default)]
struct Known<'c> {
cached: BTreeMap<crate::config::EvalId, Claim>,
previous: BTreeMap<(crate::config::EvalId, crate::types::DefinitionHash), store::Execution>,
latest: BTreeMap<crate::config::EvalId, LastRequest>,
waiting: Vec<store::RequestView>,
unreadable: Vec<store::Unreadable>,
keys: BTreeMap<&'c crate::config::EvalId, cache::Key>,
}
pub(crate) struct Progress<'a> {
pub view: StatusView,
pub cached: &'a BTreeMap<crate::config::EvalId, Claim>,
pub checking: BTreeSet<&'a crate::config::EvalId>,
}
pub(crate) type Publish<'p> = &'p mut (dyn for<'a> FnMut(Progress<'a>) + Send);
pub async fn status(
repo: &Path,
state_dir: Option<&Path>,
selection: &Selection,
options: &VerifyOptions,
cancellation: CancellationToken,
) -> Result<StatusView, String> {
super::fingerprint_parallelism(options)?;
let config = read_workspace_config(repo).map_err(|error| error.to_string())?;
scan(config, state_dir, selection, options, cancellation, None)
.await
.map(|(view, _)| view)
}
pub(crate) async fn scan(
config: crate::config::RepoConfig,
state_dir: Option<&Path>,
selection: &Selection,
options: &VerifyOptions,
cancellation: CancellationToken,
publish: Option<Publish<'_>>,
) -> Result<(StatusView, BTreeMap<crate::config::EvalId, Claim>), String> {
let parallelism = super::fingerprint_parallelism(options)?;
let config = select_profiles(
config,
selection,
options.profile.as_ref(),
options.recursive,
)?;
let graph = Graph::new(&config).map_err(|error| error.to_string())?;
let selected = selection.resolve(&config)?;
let required: BTreeSet<_> = graph
.dependency_closure(&selected.roots)
.map_err(|error| error.to_string())?
.into_iter()
.collect();
let selected_eval_ids: Vec<_> = selected.evals.iter().map(|eval| eval.id.clone()).collect();
let included_eval_ids: Vec<_> = selection
.included_evals(&config, options.recursive)?
.iter()
.map(|eval| eval.id.clone())
.collect();
let state = store::state_dir(state_dir)?;
let remote = if options.force {
None
} else {
Session::open(Some(&state), Some(&config.root))?
};
let scope = Scope {
config: &config,
graph: &graph,
selection,
options,
required: &required,
selected_eval_ids: &selected_eval_ids,
included_eval_ids: &included_eval_ids,
state: &state,
remote: remote.as_deref(),
};
let targets = cache::fingerprint_targets(&config, &required);
let fingerprints = match publish {
None => {
cache::prepare(&config, targets, &state, ¶llelism, cancellation.clone()).await?
}
Some(publish) => {
scope
.progressively(targets, ¶llelism, cancellation.clone(), publish)
.await?
}
};
let known = scope.read(&fingerprints).await?;
if cancellation.is_cancelled() {
return Err("Project preparation was cancelled.".into());
}
let view = scope.assemble(&fingerprints, &known)?;
Ok((view, known.cached))
}
struct Scope<'a> {
config: &'a crate::config::RepoConfig,
graph: &'a Graph<'a>,
selection: &'a Selection,
options: &'a VerifyOptions,
required: &'a BTreeSet<&'a crate::config::ArtifactName>,
selected_eval_ids: &'a [crate::config::EvalId],
included_eval_ids: &'a [crate::config::EvalId],
state: &'a Path,
remote: Option<&'a Session>,
}
type Fingerprints<'a> = BTreeMap<&'a crate::config::ArtifactName, cache::PreparedFingerprint>;
impl<'a> Scope<'a> {
async fn progressively(
&self,
targets: BTreeSet<&'a crate::config::ArtifactName>,
parallelism: &cache::Parallelism,
cancellation: CancellationToken,
publish: Publish<'_>,
) -> Result<Fingerprints<'a>, String> {
let mut fingerprints = Fingerprints::new();
let first = Known::default();
publish(Progress {
view: self.assemble(&fingerprints, &first)?,
cached: &first.cached,
checking: self.checking(&fingerprints, &first.cached),
});
let mut known = self.read(&fingerprints).await?;
let (ready, mut arrived) = tokio::sync::mpsc::unbounded_channel();
let preparation = cancellation.child_token();
let targets = self
.graph
.components()
.iter()
.flat_map(|component| &component.artifacts)
.filter_map(|id| targets.get(*id).copied());
let preparing = cache::prepare_each(
self.config,
targets,
self.state,
parallelism,
preparation.clone(),
move |id, fingerprint| {
let _ = ready.send((id, fingerprint));
},
);
let publishing = async {
while let Some((id, fingerprint)) = arrived.recv().await {
let started = tokio::time::Instant::now();
fingerprints.insert(id, fingerprint);
while let Ok((id, fingerprint)) = arrived.try_recv() {
fingerprints.insert(id, fingerprint);
}
self.absorb(&mut known, &fingerprints).await?;
publish(Progress {
view: self.assemble(&fingerprints, &known)?,
cached: &known.cached,
checking: self.checking(&fingerprints, &known.cached),
});
tokio::time::sleep(started.elapsed()).await;
}
Ok::<_, String>(())
};
let publishing = async {
let published = publishing.await;
if published.is_err() {
preparation.cancel();
}
published
};
let (prepared, published) = tokio::join!(preparing, publishing);
prepared?;
published?;
Ok(fingerprints)
}
async fn read(&self, fingerprints: &Fingerprints<'a>) -> Result<Known<'a>, String> {
let mut known = Known {
latest: store::read_latest_requests(self.state, &self.config.root).await?,
waiting: store::read_waiting(self.state, Some(&self.config.root)).await?,
unreadable: store::read_unreadable(self.state, Some(&self.config.root)).await?,
..Known::default()
};
self.absorb(&mut known, fingerprints).await?;
Ok(known)
}
async fn absorb(
&self,
known: &mut Known<'a>,
fingerprints: &Fingerprints<'a>,
) -> Result<(), String> {
let new: BTreeMap<&'a crate::config::EvalId, cache::Key> = self
.config
.evals
.iter()
.filter(|eval| !known.keys.contains_key(&eval.id))
.filter_map(|eval| {
Some((
&eval.id,
cache::eval_key(self.config, eval, fingerprints).ok()?,
))
})
.collect();
let selected: BTreeSet<_> = self.selected_eval_ids.iter().collect();
let keys: BTreeMap<_, _> = new
.iter()
.filter(|(id, _)| !(self.options.force && selected.contains(**id)))
.map(|(id, key)| (*id, key))
.collect();
let mut cached = store::read_profiled_executions(
self.state,
self.config
.evals
.iter()
.filter_map(|eval| {
keys.get(&eval.id).map(|key| {
(
eval.id.clone(),
key.value.clone(),
eval.declaration.profile().into(),
)
})
})
.collect(),
)
.await?;
if let Some(remote) = self.remote {
let all: Vec<_> = keys
.values()
.map(|key| key.value.clone())
.collect::<BTreeSet<_>>()
.into_iter()
.collect();
for execution in remote.lookup(&all).await? {
let key = execution.key.clone().expect("remote key");
for (id, _) in keys.iter().filter(|(_, candidate)| candidate.value == key) {
let newer = match cached.get(*id) {
Some(Claim::Reuse(local)) => execution.completed_after(local),
_ => true,
};
if newer {
cached.insert((*id).clone(), Claim::Reuse(Box::new(execution.clone())));
}
}
}
}
let stale: Vec<_> = keys
.iter()
.filter(|(id, _)| !matches!(cached.get(**id), Some(Claim::Reuse(_))))
.map(|(id, key)| ((*id).clone(), key.eval_def_hash.clone()))
.collect();
known
.previous
.extend(store::read_latest_cached(self.state, &stale).await?);
known.cached.extend(cached);
known.keys.extend(new);
Ok(())
}
fn assemble(
&self,
fingerprints: &Fingerprints<'a>,
known: &Known<'a>,
) -> Result<StatusView, String> {
view::assemble(
self.config,
self.graph,
self.selection,
self.options,
Snapshot {
required: self.required,
selected_eval_ids: self.selected_eval_ids,
included_eval_ids: self.included_eval_ids,
fingerprints,
known,
},
)
}
fn checking(
&self,
fingerprints: &Fingerprints<'a>,
cached: &BTreeMap<crate::config::EvalId, Claim>,
) -> BTreeSet<&'a crate::config::EvalId> {
let evals: BTreeMap<_, _> = self
.config
.evals
.iter()
.map(|eval| (&eval.id, eval))
.collect();
let mut settled = BTreeSet::new();
let mut checking = BTreeSet::new();
for component in self.graph.components() {
for artifact in component
.artifacts
.iter()
.filter(|id| self.required.contains(*id))
{
for id in &self.graph.artifacts()[*artifact].evals {
let keyed =
cache::dependencies(self.config, evals[id])
.into_iter()
.all(|input| {
self.config.artifacts[input].fingerprint.is_none()
|| fingerprints.contains_key(input)
});
let decided = keyed
&& (matches!(cached.get(*id), Some(Claim::Reuse(_)))
|| self
.graph
.gates(id)
.iter()
.all(|gate| settled.contains(gate)));
if decided {
settled.insert(*id);
} else {
checking.insert(*id);
}
}
}
}
checking
}
}
impl Counts {
fn action(&mut self, action: VerifyAction) {
match action {
VerifyAction::Execute => self.execute += 1,
VerifyAction::Reuse => self.reuse += 1,
VerifyAction::Wait => self.wait += 1,
VerifyAction::Blocked => self.blocked += 1,
}
}
}