use crate::backend::native::{
graph_file::GraphFile,
types::{NativeBackendError, NativeResult},
v2::export::ExportMode,
v2::wal::recovery::states::{Authority, RecoveryState as ExplicitRecoveryState},
};
use std::path::Path;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PlannerDecision {
pub export_mode: ExportMode,
pub reasoning: DecisionReason,
pub graph_stable: bool,
pub wal_state: WalAnalysis,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DecisionReason {
SnapshotOptimal,
SnapshotRequired,
WalActiveTransactions,
WalDirtyState,
WalCorruption,
GraphCorruption,
WalFallback,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WalAnalysis {
pub exists: bool,
pub size_bytes: u64,
pub has_active_transactions: bool,
pub recovery_state: ExplicitRecoveryState,
pub authority: Authority,
}
pub struct ExportPlanner;
impl ExportPlanner {
pub fn analyze_export_strategy(graph_path: &Path) -> NativeResult<PlannerDecision> {
let wal_state = Self::analyze_wal_state(graph_path)?;
let graph_stable = Self::analyze_graph_stability(graph_path)?;
let (export_mode, reasoning) = Self::apply_decision_rules(&wal_state, graph_stable);
Ok(PlannerDecision {
export_mode,
reasoning,
graph_stable,
wal_state,
})
}
pub fn is_snapshot_advisable(graph_path: &Path) -> NativeResult<bool> {
let decision = Self::analyze_export_strategy(graph_path)?;
Ok(matches!(decision.export_mode, ExportMode::Snapshot))
}
fn analyze_wal_state(graph_path: &Path) -> NativeResult<WalAnalysis> {
let wal_path = graph_path.with_extension("wal");
let exists = wal_path.exists();
let size_bytes = if exists {
std::fs::metadata(&wal_path)
.map_err(|e| NativeBackendError::Io(e))?
.len()
} else {
0
};
let has_active_transactions = exists && size_bytes > 0;
let recovery_state = if has_active_transactions {
ExplicitRecoveryState::DirtyShutdown
} else {
ExplicitRecoveryState::CleanShutdown
};
let authority = if recovery_state == ExplicitRecoveryState::CleanShutdown {
Authority::GraphFile
} else {
Authority::WAL
};
Ok(WalAnalysis {
exists,
size_bytes,
has_active_transactions,
recovery_state,
authority,
})
}
fn analyze_graph_stability(graph_path: &Path) -> NativeResult<bool> {
let mut graph_file = match GraphFile::open(graph_path) {
Ok(file) => file,
Err(_) => return Ok(false), };
if graph_file.is_transaction_active() {
return Ok(false);
}
if graph_file.validate_file_size().is_err() {
return Ok(false);
}
if graph_file.verify_commit_marker().is_err() {
return Ok(false);
}
let header = graph_file.persistent_header();
#[allow(clippy::absurd_extreme_comparisons)]
if header.node_count < 0 || header.edge_count < 0 {
return Ok(false);
}
Ok(true)
}
fn apply_decision_rules(
wal_state: &WalAnalysis,
graph_stable: bool,
) -> (ExportMode, DecisionReason) {
if graph_stable && !wal_state.exists {
return (ExportMode::Snapshot, DecisionReason::SnapshotOptimal);
}
if graph_stable
&& wal_state.exists
&& wal_state.size_bytes == 0
&& wal_state.recovery_state == ExplicitRecoveryState::CleanShutdown
{
return (ExportMode::Snapshot, DecisionReason::SnapshotOptimal);
}
if wal_state.has_active_transactions {
return (
ExportMode::CheckpointAligned,
DecisionReason::WalActiveTransactions,
);
}
if wal_state.recovery_state == ExplicitRecoveryState::DirtyShutdown {
return (ExportMode::LsnBounded, DecisionReason::WalDirtyState);
}
if wal_state.exists && wal_state.authority == Authority::Unrecoverable {
return (ExportMode::Full, DecisionReason::WalCorruption);
}
if !graph_stable {
return (ExportMode::Full, DecisionReason::GraphCorruption);
}
if graph_stable
&& wal_state.exists
&& wal_state.size_bytes > 0
&& wal_state.recovery_state == ExplicitRecoveryState::CleanShutdown
{
return (ExportMode::Snapshot, DecisionReason::SnapshotRequired);
}
(ExportMode::CheckpointAligned, DecisionReason::WalFallback)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::NamedTempFile;
#[test]
fn test_planner_decision_rules() {
let wal_state = WalAnalysis {
exists: false,
size_bytes: 0,
has_active_transactions: false,
recovery_state: ExplicitRecoveryState::CleanShutdown,
authority: Authority::GraphFile,
};
let graph_stable = true;
let (export_mode, reasoning) =
ExportPlanner::apply_decision_rules(&wal_state, graph_stable);
assert_eq!(export_mode, ExportMode::Snapshot);
assert_eq!(reasoning, DecisionReason::SnapshotOptimal);
}
#[test]
fn test_planner_active_transactions() {
let wal_state = WalAnalysis {
exists: true,
size_bytes: 1024,
has_active_transactions: true,
recovery_state: ExplicitRecoveryState::DirtyShutdown,
authority: Authority::WAL,
};
let graph_stable = true;
let (export_mode, reasoning) =
ExportPlanner::apply_decision_rules(&wal_state, graph_stable);
assert_eq!(export_mode, ExportMode::CheckpointAligned);
assert_eq!(reasoning, DecisionReason::WalActiveTransactions);
}
#[test]
fn test_planner_dirty_wal() {
let wal_state = WalAnalysis {
exists: true,
size_bytes: 2048,
has_active_transactions: false,
recovery_state: ExplicitRecoveryState::DirtyShutdown,
authority: Authority::WAL,
};
let graph_stable = true;
let (export_mode, reasoning) =
ExportPlanner::apply_decision_rules(&wal_state, graph_stable);
assert_eq!(export_mode, ExportMode::LsnBounded);
assert_eq!(reasoning, DecisionReason::WalDirtyState);
}
#[test]
fn test_planner_unstable_graph() {
let wal_state = WalAnalysis {
exists: false,
size_bytes: 0,
has_active_transactions: false,
recovery_state: ExplicitRecoveryState::CleanShutdown,
authority: Authority::GraphFile,
};
let graph_stable = false;
let (export_mode, reasoning) =
ExportPlanner::apply_decision_rules(&wal_state, graph_stable);
assert_eq!(export_mode, ExportMode::Full);
assert_eq!(reasoning, DecisionReason::GraphCorruption);
}
#[test]
fn test_planner_deterministic() {
let wal_state = WalAnalysis {
exists: false,
size_bytes: 0,
has_active_transactions: false,
recovery_state: ExplicitRecoveryState::CleanShutdown,
authority: Authority::GraphFile,
};
let graph_stable = true;
let result1 = ExportPlanner::apply_decision_rules(&wal_state, graph_stable);
let result2 = ExportPlanner::apply_decision_rules(&wal_state, graph_stable);
assert_eq!(result1, result2);
}
#[test]
fn test_planner_is_snapshot_advisable() {
let temp_file = NamedTempFile::new().unwrap();
let graph_path = temp_file.path().to_path_buf();
let _graph_file = GraphFile::create(&graph_path).unwrap();
let result = ExportPlanner::is_snapshot_advisable(&graph_path);
assert!(result.is_ok());
}
}