use std::path::Path;
use crate::engine::paths::StoragePaths;
use linkrs_core::{StorageError, StorageResult};
use linkrs_sync::checkpoint_manifest::CheckpointManifestManager;
use linkrs_transaction::wal::recovery::RecoveryStats;
use linkrs_transaction::wal::Lsn;
use super::context::GraphStorageContext;
mod checkpoint;
mod recovery;
#[cfg(test)]
mod tests;
pub(crate) use checkpoint::{
auto_checkpoint_if_needed, auto_flush_if_needed, cleanup_snapshots, compact_transactional,
create_checkpoint, create_checkpoint_with_guard, load_latest_checkpoint,
persistence_diagnostics, should_checkpoint, should_flush, snapshot_stats, verify_snapshot,
};
pub(crate) use recovery::{needs_recovery, recover_from_wal, recover_from_wal_with_config};
fn load_schema_and_index_metadata(ctx: &GraphStorageContext) -> StorageResult<()> {
if let Some(path) = ctx.work_dir().as_ref() {
let paths = StoragePaths::new(path.clone());
let latest_checkpoint = latest_published_checkpoint_dir(path)?;
let schema_paths = latest_checkpoint
.as_ref()
.map(|checkpoint| StoragePaths::new(checkpoint).schema_file())
.into_iter()
.chain(std::iter::once(paths.schema_file()));
for schema_path in schema_paths {
if schema_path.exists() {
ctx.schema_manager().load_schema(&schema_path)?;
break;
}
}
let index_paths = latest_checkpoint
.as_ref()
.map(|checkpoint| StoragePaths::new(checkpoint).index_meta_file())
.into_iter()
.chain(std::iter::once(paths.index_meta_file()));
for index_path in index_paths {
if index_path.exists() {
ctx.index_metadata_manager().load_indexes(&index_path)?;
break;
}
}
ctx.load_migration_history()?;
}
Ok(())
}
fn latest_published_checkpoint_dir(work_dir: &Path) -> StorageResult<Option<std::path::PathBuf>> {
let checkpoint_root = work_dir.join("checkpoint");
if !checkpoint_root.exists() {
return Ok(None);
}
let manifest_manager = CheckpointManifestManager::new(checkpoint_root.join("manifests"));
manifest_manager
.load_latest()
.map_err(StorageError::db_error)
.map(|manifest| manifest.map(|manifest| manifest.storage_snapshot.path))
}
fn restore_full_state_from_disk(ctx: &GraphStorageContext) -> StorageResult<()> {
if let Some(path) = ctx.work_dir().as_ref() {
let paths = StoragePaths::new(path.clone());
ctx.restore_from_checkpoint(path)?;
ctx.user_storage().load_from_dir(paths.data_dir())?;
let index_path = paths.indexes_dir();
if index_path.exists() {
ctx.index_data_manager().write().load(&index_path)?;
}
ctx.register_loaded_native_indexes()?;
let restored = read_version_high_water(&paths.data_dir()).unwrap_or(1);
ctx.version_manager().init_ts(restored.max(1));
}
Ok(())
}
const VERSION_META_FILE: &str = "version.meta";
fn write_version_high_water(data_dir: &Path, write_ts: u64) -> StorageResult<()> {
let content = format!("write_ts={}\n", write_ts);
std::fs::write(data_dir.join(VERSION_META_FILE), content)
.map_err(|e| StorageError::io_error(format!("Failed to write version.meta: {e}")))?;
Ok(())
}
fn read_version_high_water(data_dir: &Path) -> StorageResult<u64> {
let content = std::fs::read_to_string(data_dir.join(VERSION_META_FILE))
.map_err(|e| StorageError::io_error(format!("Failed to read version.meta: {e}")))?;
for line in content.lines() {
if let Some(value) = line.strip_prefix("write_ts=") {
return value
.trim()
.parse::<u64>()
.map_err(|e| StorageError::parse_error(format!("Invalid version.meta: {e}")));
}
}
Err(StorageError::parse_error(
"version.meta has no write_ts entry".to_string(),
))
}
pub(crate) fn bootstrap_from_disk(ctx: &GraphStorageContext) -> StorageResult<()> {
load_schema_and_index_metadata(ctx)?;
super::schema_writer::ensure_graph_types_from_schema(ctx)?;
let checkpoint_info = load_latest_checkpoint(ctx)?;
if let Some(ref info) = checkpoint_info {
if let Some(persistence) = ctx.persistence() {
let coordinator = persistence.read();
if let Some(wal_manager) = coordinator.wal_manager() {
wal_manager
.write()
.set_recovery_baseline_lsn(info.lsn)
.map_err(|error| {
StorageError::wal_error(format!(
"Failed to restore WAL checkpoint baseline: {}",
error
))
})?;
}
}
ctx.version_manager().init_ts(info.timestamp);
} else {
restore_full_state_from_disk(ctx)?;
ctx.version_manager().init_ts(1);
}
Ok(())
}
pub(crate) fn initialize_with_recovery(
ctx: &GraphStorageContext,
) -> StorageResult<Option<RecoveryStats>> {
bootstrap_from_disk(ctx)?;
if !needs_recovery(ctx) {
super::serial::seed_serial_allocators(ctx)?;
ctx.ensure_checkpoint_scheduler();
return Ok(None);
}
log::info!("WAL recovery needed, starting recovery...");
let stats = recover_from_wal(ctx)?;
log::info!(
"WAL recovery completed: {} entries replayed in {}ms",
stats.wal_entries_replayed,
stats.recovery_time_ms
);
super::serial::seed_serial_allocators(ctx)?;
ctx.ensure_checkpoint_scheduler();
Ok(Some(stats))
}
pub(crate) fn save_data(ctx: &GraphStorageContext) -> StorageResult<()> {
let paths = ctx
.storage_paths()
.ok_or_else(|| StorageError::db_error("No work directory configured".to_string()))?;
save_data_to_dir(ctx, paths.root())
}
pub(crate) fn save_data_to_dir(ctx: &GraphStorageContext, dir: &Path) -> StorageResult<()> {
use std::fs::{self};
let paths = StoragePaths::new(dir);
let data_dir = paths.data_dir();
fs::create_dir_all(&data_dir)?;
ctx.flush_tables_to_dir(&data_dir)?;
ctx.user_storage().save_to_dir(&data_dir)?;
write_version_high_water(&data_dir, ctx.version_manager().write_timestamp())?;
if let Some(persistence) = ctx.persistence().as_ref() {
let wal_lsn = {
let coordinator = persistence.read();
coordinator
.wal_manager()
.map(|w| w.read().current_lsn())
.unwrap_or(Lsn::ZERO)
};
persistence.read().mark_flushed(wal_lsn);
}
log::info!("Data saved to {:?}", data_dir);
Ok(())
}
pub(crate) fn flush(ctx: &GraphStorageContext) -> StorageResult<()> {
save_data(ctx)
}
pub(crate) fn load_from_disk(ctx: &GraphStorageContext) -> StorageResult<()> {
load_schema_and_index_metadata(ctx)?;
super::schema_writer::ensure_graph_types_from_schema(ctx)?;
restore_full_state_from_disk(ctx)?;
super::serial::seed_serial_allocators(ctx)
}
pub(crate) fn save_to_disk(ctx: &GraphStorageContext) -> StorageResult<()> {
if let Some(path) = ctx.work_dir().as_ref() {
let paths = StoragePaths::new(path.clone());
std::fs::create_dir_all(paths.root()).map_err(|e| StorageError::io_error(e.to_string()))?;
let schema_dir = paths.schema_dir();
std::fs::create_dir_all(&schema_dir).map_err(|e| StorageError::io_error(e.to_string()))?;
let schema_path = paths.schema_file();
ctx.schema_manager()
.set_serial_next(super::serial::serial_next_snapshot(ctx));
ctx.schema_manager().save_schema(&schema_path)?;
let index_meta_dir = paths.index_meta_dir();
std::fs::create_dir_all(&index_meta_dir)
.map_err(|e| StorageError::io_error(e.to_string()))?;
let index_meta_path = paths.index_meta_file();
ctx.index_metadata_manager()
.save_indexes(&index_meta_path)?;
ctx.save_migration_history()?;
save_data_to_dir(ctx, paths.root())?;
let index_path = paths.indexes_dir();
std::fs::create_dir_all(&index_path).map_err(|e| StorageError::io_error(e.to_string()))?;
ctx.index_data_manager().read().flush(&index_path)?;
}
Ok(())
}