use serde::Serialize;
use thiserror::Error;
use graphrefly_structures::Lifecycle;
use crate::codec::CodecError;
use crate::wal::ChecksumError;
#[derive(Debug, Error)]
pub enum StorageError {
#[error("storage tier {tier:?} does not support list_by_prefix; WAL replay requires it")]
BackendNoListSupport { tier: String },
#[error("codec mismatch: expected {expected:?}, found {found:?}")]
CodecMismatch { expected: String, found: String },
#[error("codec error: {0}")]
Codec(#[from] CodecError),
#[error("backend error: {message}")]
BackendError {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
},
}
impl From<ChecksumError> for StorageError {
fn from(e: ChecksumError) -> Self {
match e {
ChecksumError::CanonicalJsonFailed(err) => {
StorageError::Codec(CodecError::Encode(err.to_string()))
}
}
}
}
#[derive(Debug, Error)]
pub enum RestoreError {
#[error("restore phase {lifecycle:?} failed at frame_seq={frame_seq}: {message}")]
PhaseFailed {
lifecycle: Lifecycle,
frame_seq: u64,
message: String,
},
#[error("torn write mid-stream at frame_seq={frame_seq}: {reason}")]
TornWriteMidStream { frame_seq: u64, reason: String },
#[error("no mode=full baseline in snapshot tier; replay requires a baseline")]
BaselineMissing,
#[error("codec mismatch at frame_seq={frame_seq}: expected {expected:?}, found {found:?}")]
CodecMismatch {
frame_seq: u64,
expected: String,
found: String,
},
#[error("restore requires a wal_tier when the snapshot tier does not carry WAL frames")]
WalTierRequired,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct PhaseStat {
pub lifecycle: Lifecycle,
pub frames: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct RestoreResult {
pub replayed_frames: u64,
pub skipped_frames: u64,
pub final_seq: u64,
pub phases: Vec<PhaseStat>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn restore_result_discriminants_present() {
let r = RestoreResult {
replayed_frames: 3,
skipped_frames: 0,
final_seq: 42,
phases: vec![
PhaseStat {
lifecycle: Lifecycle::Spec,
frames: 1,
},
PhaseStat {
lifecycle: Lifecycle::Data,
frames: 2,
},
],
};
assert_eq!(r.replayed_frames, 3);
assert_eq!(r.phases.len(), 2);
assert_eq!(r.phases[0].lifecycle, Lifecycle::Spec);
}
#[test]
fn storage_error_display_carries_tier_name() {
let e = StorageError::BackendNoListSupport {
tier: "memory".into(),
};
assert!(e.to_string().contains("memory"));
}
#[test]
fn restore_error_phase_failed_includes_seq() {
let e = RestoreError::PhaseFailed {
lifecycle: Lifecycle::Data,
frame_seq: 17,
message: "downstream invariant broken".into(),
};
let s = e.to_string();
assert!(s.contains("17"), "frame_seq missing from display: {s}");
assert!(s.contains("Data"), "lifecycle missing from display: {s}");
}
}