use super::{MAX_TASK_ACTION_PREDICTIONS, TASK_ACTION_MANIFEST_VERSION};
use crate::{ActionPrediction, CacheDigest, TaskActionManifest};
use eyre::{Context, Result, bail};
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};
pub fn is_task_identity(task: &str) -> bool {
task.len() == 64
&& task
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
}
pub(super) fn validate_task_identity(task: &str) -> Result<()> {
if !is_task_identity(task) {
bail!("invalid task action identity");
}
Ok(())
}
pub(super) fn task_manifest_dir(store: &Path) -> PathBuf {
store.join("task-manifests").join("v1")
}
pub fn task_manifest_actions(store: &Path, task: &str) -> Result<Vec<CacheDigest>> {
validate_task_identity(task)?;
let path = task_manifest_dir(store).join(format!("{task}.json"));
let bytes = match fs::read(&path) {
Ok(bytes) => bytes,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(error) => {
return Err(error).wrap_err_with(|| format!("failed to read {}", path.display()));
}
};
let Ok(manifest) = serde_json::from_slice::<TaskActionManifest>(&bytes) else {
return Ok(Vec::new());
};
if validate_task_manifest(&manifest, task).is_err() {
return Ok(Vec::new());
}
Ok(manifest
.predictions
.into_iter()
.map(|prediction| prediction.action)
.collect())
}
pub(super) fn validate_task_manifest(manifest: &TaskActionManifest, task: &str) -> Result<()> {
if manifest.task == task && manifest.validate() {
Ok(())
} else {
bail!("invalid task action manifest")
}
}
pub(super) fn update_task_predictions(
predictions: Vec<ActionPrediction>,
updates: &BTreeMap<CacheDigest, ActionPrediction>,
protected: &BTreeSet<CacheDigest>,
) -> Result<Vec<ActionPrediction>> {
update_task_predictions_in_order(predictions, updates.values().cloned().collect(), protected)
}
pub fn update_task_predictions_in_order(
predictions: Vec<ActionPrediction>,
mut updates: Vec<ActionPrediction>,
protected: &BTreeSet<CacheDigest>,
) -> Result<Vec<ActionPrediction>> {
if protected.len() > MAX_TASK_ACTION_PREDICTIONS {
bail!("this task run contains too many action predictions");
}
let mut update_invocations = BTreeSet::new();
updates = updates
.into_iter()
.rev()
.filter(|prediction| update_invocations.insert(prediction.invocation.clone()))
.collect();
updates.reverse();
let mut predictions: Vec<_> = predictions
.into_iter()
.filter(|prediction| !update_invocations.contains(&prediction.invocation))
.collect();
predictions.extend(updates);
let mut excess = predictions
.len()
.saturating_sub(MAX_TASK_ACTION_PREDICTIONS);
let mut kept = Vec::with_capacity(predictions.len() - excess);
for prediction in predictions {
if excess > 0 && !protected.contains(&prediction.invocation) {
excess -= 1;
} else {
kept.push(prediction);
}
}
Ok(kept)
}
pub(super) fn merge_task_manifests(
task: &str,
base: TaskActionManifest,
update: TaskActionManifest,
required: &BTreeSet<CacheDigest>,
fallbacks: &BTreeSet<CacheDigest>,
) -> Result<TaskActionManifest> {
validate_task_manifest(&base, task)?;
validate_task_manifest(&update, task)?;
let base_predictions: BTreeMap<_, _> = base
.predictions
.iter()
.map(|prediction| (&prediction.invocation, prediction))
.collect();
let updates: Vec<_> = update
.predictions
.iter()
.filter(|prediction| {
required.contains(&prediction.invocation)
|| (fallbacks.contains(&prediction.invocation)
&& base_predictions
.get(&prediction.invocation)
.is_none_or(|base| *base == *prediction))
})
.cloned()
.collect();
let protected = required
.iter()
.cloned()
.chain(
updates
.iter()
.map(|prediction| prediction.invocation.clone()),
)
.collect();
let mut predictions: Vec<_> = update
.predictions
.into_iter()
.filter(|prediction| !base_predictions.contains_key(&prediction.invocation))
.collect();
predictions.extend(base.predictions);
let predictions = update_task_predictions_in_order(predictions, updates, &protected)?;
let manifest = TaskActionManifest {
version: TASK_ACTION_MANIFEST_VERSION,
task: task.to_owned(),
predictions,
};
validate_task_manifest(&manifest, task)?;
Ok(manifest)
}