use super::encoder::write_snapshot_stream as encode_snapshot;
use crate::error::SearchError;
use crate::hnsw::VectorIndex;
use std::ffi::OsString;
use std::fs::OpenOptions;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
pub(super) fn write_snapshot(index: &VectorIndex, target: &Path) -> Result<(), SearchError> {
let temporary = temporary_path(target, "tmp")?;
let result = (|| {
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&temporary)
.map_err(|error| SearchError::storage("create snapshot", &error))?;
encode_snapshot(index, &mut file)?;
file.sync_all()
.map_err(|error| SearchError::storage("flush snapshot", &error))?;
drop(file);
replace_snapshot(&temporary, target)
})();
if result.is_err() {
let _ = std::fs::remove_file(&temporary);
}
result
}
pub(super) use super::encoder::write_snapshot_stream;
fn temporary_path(target: &Path, label: &str) -> Result<PathBuf, SearchError> {
let file_name = target
.file_name()
.ok_or(SearchError::InvalidConfig("snapshot path has no file name"))?;
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |duration| duration.as_nanos());
let mut temporary_name = OsString::from(file_name);
temporary_name.push(format!(".{label}-{}-{nonce}", std::process::id()));
Ok(target.with_file_name(temporary_name))
}
fn replace_snapshot(temporary: &Path, target: &Path) -> Result<(), SearchError> {
match std::fs::rename(temporary, target) {
Ok(()) => Ok(()),
Err(_first_error) if target.exists() => {
let backup = temporary_path(target, "backup")?;
std::fs::rename(target, &backup)
.map_err(|error| SearchError::storage("backup old snapshot", &error))?;
match std::fs::rename(temporary, target) {
Ok(()) => {
let _ = std::fs::remove_file(backup);
Ok(())
}
Err(error) => {
let _ = std::fs::rename(&backup, target);
Err(SearchError::storage("replace snapshot", &error))
}
}
}
Err(error) => Err(SearchError::storage("replace snapshot", &error)),
}
}