use crate::pipeline::PipelineError;
use serde::Serialize;
use serde::de::DeserializeOwned;
use sha2::{Digest, Sha256};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
static TEMP_SEQ: AtomicU64 = AtomicU64::new(0);
#[cfg_attr(docsrs, doc(cfg(feature = "cache")))]
pub fn cache_key<T: Serialize>(step: &str, item: &T) -> Result<String, PipelineError> {
let json = serde_json::to_vec(item).map_err(task_failed)?;
Ok(hex_encode(&content_hash(step, &json)))
}
#[cfg_attr(docsrs, doc(cfg(feature = "cache")))]
pub fn cache_file<T: Serialize>(
cache_dir: impl AsRef<Path>,
step: &str,
item: &T,
) -> Result<PathBuf, PipelineError> {
let key = cache_key(step, item)?;
Ok(entry_path(cache_dir.as_ref(), step, &key))
}
pub(crate) fn resolve<T, U, F>(
step: &str,
cache_dir: &Path,
item: T,
compute: &F,
logged: &mut bool,
) -> Result<U, PipelineError>
where
T: Serialize,
U: Serialize + DeserializeOwned,
F: Fn(T) -> U,
{
let json = serde_json::to_vec(&item).map_err(task_failed)?;
let path = entry_path(cache_dir, step, &hex_encode(&content_hash(step, &json)));
if let Some(hit) = read_value(&path) {
return Ok(hit);
}
let value = compute(item);
if let Err(err) = persist(&path, &value) {
log_once(logged, &err);
}
Ok(value)
}
fn task_failed(err: impl std::fmt::Display) -> PipelineError {
PipelineError::TaskFailed {
stage: "cached_task",
message: err.to_string(),
}
}
fn entry_path(cache_dir: &Path, step: &str, key: &str) -> PathBuf {
cache_dir
.join(step_prefix(step))
.join(format!("{key}.json"))
}
fn step_prefix(step: &str) -> String {
let hex = hex_encode(&sha256(step.as_bytes()));
hex[..16].to_owned()
}
fn content_hash(step: &str, json: &[u8]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(sha256(step.as_bytes()));
hasher.update(json);
finalize(hasher)
}
fn sha256(bytes: &[u8]) -> [u8; 32] {
finalize(Sha256::new().chain_update(bytes))
}
fn finalize(hasher: Sha256) -> [u8; 32] {
let digest = hasher.finalize();
let mut out = [0u8; 32];
out.copy_from_slice(&digest);
out
}
fn hex_encode(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(bytes.len() * 2);
for byte in bytes {
out.push(HEX[usize::from(byte >> 4)] as char);
out.push(HEX[usize::from(byte & 0x0f)] as char);
}
out
}
fn read_value<U: DeserializeOwned>(path: &Path) -> Option<U> {
let bytes = std::fs::read(path).ok()?;
serde_json::from_slice(&bytes).ok()
}
fn persist<U: Serialize>(path: &Path, value: &U) -> Result<(), String> {
let bytes = serde_json::to_vec(value).map_err(|err| err.to_string())?;
write_atomic(path, &bytes).map_err(|err| err.to_string())
}
fn log_once(logged: &mut bool, err: &dyn std::fmt::Display) {
if *logged {
return;
}
*logged = true;
eprintln!("olympipe-rs: [cached_task] cache write failed: {err}");
}
fn write_atomic(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
let parent = path
.parent()
.filter(|dir| !dir.as_os_str().is_empty())
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"cache entry path has no directory",
)
})?;
std::fs::create_dir_all(parent)?;
let file_name = path.file_name().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"cache entry path has no file name",
)
})?;
let seq = TEMP_SEQ.fetch_add(1, Ordering::Relaxed);
let mut tmp_name = std::ffi::OsString::from(".");
tmp_name.push(file_name);
tmp_name.push(format!(".{}.{seq}.tmp", std::process::id()));
let tmp = parent.join(tmp_name);
let written = (|| -> std::io::Result<()> {
let mut file = std::fs::File::create(&tmp)?;
file.write_all(bytes)?;
file.sync_all()?;
std::fs::rename(&tmp, path)?;
Ok(())
})();
if written.is_err() {
let _ = std::fs::remove_file(&tmp);
}
written
}