use std::hash::{BuildHasher as _, Hasher as _};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::SystemTime;
use anyhow::Context as _;
use taktwerk_core::plan::Plan;
use taktwerk_core::project::Project;
use taktwerk_core::record::{FORMAT, Invocation, Manifest, ModelInfo, Records};
use taktwerk_core::schedule::ModelAdapters;
use taktwerk_record::ServiceConfig;
use taktwerk_record::util;
use crate::setup::model_path;
pub fn engine_run() -> String {
let mut h = std::collections::hash_map::RandomState::new().build_hasher();
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default();
h.write_u128(now.as_nanos());
h.write_u32(std::process::id());
format!("{:016x}", h.finish())
}
pub fn records(
project_file: &Path,
project: &Project,
adapters: &ModelAdapters,
plan: &Plan,
invocation: &Invocation,
) -> anyhow::Result<Option<(Arc<Records>, ServiceConfig)>> {
if plan.recorders.is_empty() {
return Ok(None);
}
let bytes =
std::fs::read(project_file).with_context(|| format!("read {}", project_file.display()))?;
let name = project_file.file_stem().map_or_else(
|| "project".to_owned(),
|s| s.to_string_lossy().into_owned(),
);
let mut models = Vec::new();
for (id, model) in &project.models {
let path = model_path(project_file, &model.path);
let hash = util::sha256_package(&path)
.with_context(|| format!("hash model `{id}` at {}", path.display()))?;
models.push(ModelInfo {
id: id.clone(),
kind: model.kind.clone(),
path: model.path.display().to_string(),
name: adapters
.get(id)
.map(|a| a.interface().name.clone())
.unwrap_or_default(),
hash,
});
}
let base = Manifest {
format: FORMAT,
project: name,
project_hash: util::sha256(&bytes),
taktwerk: env!("CARGO_PKG_VERSION").to_owned(),
engine_run: engine_run(),
recorder: String::new(),
tick_s: plan.tick.as_secs_f64(),
start_time: plan.start_time,
every: 1,
buffer_s: 0.0,
rotate: Default::default(),
retain: Default::default(),
models,
columns: Vec::new(),
invocation: Some(invocation.clone()),
};
let mut manifests = Vec::new();
let mut dirs: Vec<PathBuf> = Vec::new();
for rec in &plan.recorders {
let c = &rec.config;
let manifest = Manifest {
recorder: c.id.clone(),
every: c.every,
buffer_s: c.buffer_s,
rotate: c.rotate.clone(),
retain: c.retain.clone(),
columns: Manifest::columns(rec, &plan.layout),
..base.clone()
};
manifests.push(serde_json::to_string(&manifest).context("manifest")?);
dirs.push(model_path(project_file, &c.dir));
}
let instances = plan.instances.iter().map(|i| i.id.clone()).collect();
let records = Records::new(plan.recorders.clone(), manifests);
Ok(Some((
Arc::new(records),
ServiceConfig::local(instances, dirs),
)))
}
#[cfg(test)]
#[allow(clippy::unwrap_used, reason = "tests")]
mod tests {
use super::*;
#[test]
fn engine_runs_differ_and_are_hex() {
let (a, b) = (engine_run(), engine_run());
assert_ne!(a, b);
assert_eq!(a.len(), 16);
assert!(a.chars().all(|c| c.is_ascii_hexdigit()));
}
}