use std::collections::HashMap;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use crate::storage::cas::CasStore;
use crate::storage::cas::blob::fsync_file;
use super::lease::{CURRENT_FILE, GENERATIONS_DIR, MANIFEST_FILE};
use super::manifest::{
ALL_LAYER_KINDS, LayerKind, MANIFEST_VERSION, Manifest, ModelIdentity, OPTIONAL_LAYER_KINDS,
};
const MANIFEST_PARTIAL: &str = "manifest.partial";
const CURRENT_TMP: &str = "CURRENT.tmp";
#[derive(Debug, thiserror::Error)]
pub enum WriterError {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("cas error: {0}")]
Cas(#[from] crate::storage::cas::CasError),
#[error("manifest error: {0}")]
Manifest(#[from] super::manifest::ManifestError),
#[error("cannot publish: missing layers: {0:?}")]
MissingLayers(Vec<LayerKind>),
}
pub struct GenerationWriter {
storage_root: PathBuf,
cas: Arc<Mutex<CasStore>>,
staged: HashMap<LayerKind, [u8; 32]>,
model_identity: ModelIdentity,
last_manifest: Option<Manifest>,
}
impl GenerationWriter {
pub fn new(storage_root: impl AsRef<Path>, cas: Arc<Mutex<CasStore>>) -> Self {
GenerationWriter {
storage_root: storage_root.as_ref().to_path_buf(),
cas,
staged: HashMap::new(),
model_identity: ModelIdentity {
name: String::new(),
digest: String::new(),
dimensions: 0,
},
last_manifest: None,
}
}
pub fn storage_root(&self) -> &Path {
&self.storage_root
}
pub fn set_model_identity(&mut self, identity: ModelIdentity) {
self.model_identity = identity;
}
pub fn stage(&mut self, layer: LayerKind, bytes: &[u8]) -> Result<[u8; 32], WriterError> {
let hash = {
let cas = self.cas.lock().expect("cas mutex poisoned");
cas.put(bytes)?
};
self.staged.insert(layer, hash);
Ok(hash)
}
pub fn staged_hash(&self, layer: LayerKind) -> Option<&[u8; 32]> {
self.staged.get(&layer)
}
pub fn finish_publish(&mut self) {
self.staged.clear();
}
pub fn publish(&mut self, generation_number: u64) -> Result<(), WriterError> {
self.publish_internal(generation_number, CrashPoint::None, true)
}
pub fn publish_manifest_only(&mut self, generation_number: u64) -> Result<(), WriterError> {
self.publish_internal(generation_number, CrashPoint::None, false)
}
#[doc(hidden)]
pub fn write_manifest_partial_for_test(
&mut self,
generation_number: u64,
) -> Result<(), WriterError> {
let manifest = self.build_manifest(generation_number)?;
let gen_dir = self.generation_dir(generation_number);
fs::create_dir_all(&gen_dir)?;
let partial_path = gen_dir.join(MANIFEST_PARTIAL);
let bytes = manifest.to_bytes()?;
write_and_fsync(&partial_path, &bytes)?;
Ok(())
}
#[doc(hidden)]
pub fn write_and_rename_manifest_for_test(
&mut self,
generation_number: u64,
) -> Result<(), WriterError> {
let manifest = self.build_manifest(generation_number)?;
let gen_dir = self.generation_dir(generation_number);
fs::create_dir_all(&gen_dir)?;
let partial_path = gen_dir.join(MANIFEST_PARTIAL);
let final_path = gen_dir.join(MANIFEST_FILE);
let bytes = manifest.to_bytes()?;
write_and_fsync(&partial_path, &bytes)?;
fs::rename(&partial_path, &final_path)?;
if let Ok(dir) = fs::File::open(&gen_dir) {
let _ = fsync_file(&dir);
}
Ok(())
}
#[doc(hidden)]
pub fn publish_with_simulated_crash(
&mut self,
generation_number: u64,
kill_point: u8,
) -> Result<(), WriterError> {
let crash = match kill_point {
0 => CrashPoint::AfterStagingBlobWrite,
1 => CrashPoint::AfterStagingFsync,
2 => CrashPoint::AfterBlobRename,
3 => CrashPoint::AfterManifestPartialWrite,
4 => CrashPoint::AfterManifestFsync,
5 => CrashPoint::AfterManifestRename,
6 => CrashPoint::AfterCurrentWrite,
_ => CrashPoint::None,
};
self.publish_internal(generation_number, crash, true)
}
pub fn sweep_partial_manifests(&mut self) -> Result<(), WriterError> {
let gens_dir = self.storage_root.join(GENERATIONS_DIR);
if !gens_dir.exists() {
return Ok(());
}
for entry in fs::read_dir(&gens_dir)? {
let entry = entry?;
if !entry.file_type()?.is_dir() {
continue;
}
let gen_dir = entry.path();
let partial = gen_dir.join(MANIFEST_PARTIAL);
if partial.exists() {
let _ = fs::remove_file(&partial);
}
let manifest = gen_dir.join(MANIFEST_FILE);
if !manifest.exists() {
let is_empty = fs::read_dir(&gen_dir)?.next().is_none();
if is_empty {
let _ = fs::remove_dir(&gen_dir);
}
}
}
Ok(())
}
#[doc(hidden)]
pub fn last_published_manifest(&self) -> Option<&Manifest> {
self.last_manifest.as_ref()
}
fn publish_internal(
&mut self,
generation_number: u64,
crash: CrashPoint,
update_current: bool,
) -> Result<(), WriterError> {
let missing = ALL_LAYER_KINDS
.iter()
.filter(|k| !self.staged.contains_key(k))
.copied()
.collect::<Vec<_>>();
if !missing.is_empty() {
return Err(WriterError::MissingLayers(missing));
}
if matches!(
crash,
CrashPoint::AfterStagingBlobWrite
| CrashPoint::AfterStagingFsync
| CrashPoint::AfterBlobRename
) {
return Ok(());
}
let manifest = self.build_manifest(generation_number)?;
let gen_dir = self.generation_dir(generation_number);
fs::create_dir_all(&gen_dir)?;
let partial_path = gen_dir.join(MANIFEST_PARTIAL);
let manifest_path = gen_dir.join(MANIFEST_FILE);
let manifest_bytes = manifest.to_bytes()?;
write_and_fsync(&partial_path, &manifest_bytes)?;
if crash == CrashPoint::AfterManifestPartialWrite {
return Ok(());
}
if crash == CrashPoint::AfterManifestFsync {
return Ok(());
}
fs::rename(&partial_path, &manifest_path)?;
if let Ok(dir) = fs::File::open(&gen_dir) {
let _ = fsync_file(&dir);
}
if crash == CrashPoint::AfterManifestRename {
return Ok(());
}
if update_current {
self.write_current_atomic(generation_number)?;
self.last_manifest = Some(manifest);
self.staged.clear();
} else {
self.last_manifest = Some(manifest);
}
Ok(())
}
fn build_manifest(&self, generation_number: u64) -> Result<Manifest, WriterError> {
let pdg_hash = self.staged.get(&LayerKind::Pdg).copied();
let symbols_hash = self.staged.get(&LayerKind::Symbols).copied();
let graph_fingerprint = compute_graph_fingerprint(pdg_hash, symbols_hash);
let tfidf_hash = self.staged.get(&LayerKind::Tfidf).copied();
let neural_hash = self.staged.get(&LayerKind::Neural).copied();
let search_fingerprint = compute_search_fingerprint(tfidf_hash, neural_hash);
let mut layers = HashMap::new();
for kind in ALL_LAYER_KINDS.iter().chain(OPTIONAL_LAYER_KINDS.iter()) {
if let Some(hash) = self.staged.get(kind) {
layers.insert(*kind, *hash);
}
}
Ok(Manifest {
version: MANIFEST_VERSION,
generation: generation_number,
model_identity: self.model_identity.clone(),
graph_fingerprint,
search_fingerprint,
layers,
})
}
fn generation_dir(&self, generation_number: u64) -> PathBuf {
self.storage_root
.join(GENERATIONS_DIR)
.join(generation_number.to_string())
}
fn write_current_atomic(&self, generation_number: u64) -> Result<(), WriterError> {
let tmp_path = self.storage_root.join(CURRENT_TMP);
let final_path = self.storage_root.join(CURRENT_FILE);
let content = format!("{}\n", generation_number);
write_and_fsync(&tmp_path, content.as_bytes())?;
fs::rename(&tmp_path, &final_path)?;
if let Ok(dir) = fs::File::open(&self.storage_root) {
let _ = fsync_file(&dir);
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CrashPoint {
None,
AfterStagingBlobWrite,
AfterStagingFsync,
AfterBlobRename,
AfterManifestPartialWrite,
AfterManifestFsync,
AfterManifestRename,
AfterCurrentWrite,
}
fn write_and_fsync(path: &Path, bytes: &[u8]) -> Result<(), WriterError> {
let file = fs::File::create(path)?;
let mut writer = std::io::BufWriter::new(file);
writer.write_all(bytes)?;
writer.flush()?;
fsync_file(writer.get_ref())?;
Ok(())
}
fn compute_graph_fingerprint(
pdg_hash: Option<[u8; 32]>,
symbols_hash: Option<[u8; 32]>,
) -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
if let Some(h) = pdg_hash {
hasher.update(b"pdg:");
hasher.update(&h);
}
if let Some(h) = symbols_hash {
hasher.update(b"symbols:");
hasher.update(&h);
}
hasher.finalize().into()
}
fn compute_search_fingerprint(
tfidf_hash: Option<[u8; 32]>,
neural_hash: Option<[u8; 32]>,
) -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
if let Some(h) = tfidf_hash {
hasher.update(b"tfidf:");
hasher.update(&h);
}
if let Some(h) = neural_hash {
hasher.update(b"neural:");
hasher.update(&h);
}
hasher.finalize().into()
}
#[cfg(test)]
#[path = "writer_test.rs"]
mod tests;