use std::path::{Path, PathBuf};
use linkrs_core::types::Timestamp;
use linkrs_core::wal::types::{
Lsn, WalError, WalFileHeader, WalHeader, WalRecoveryMode, WalResult, WAL_FILE_HEADER_SIZE,
WAL_HEADER_SIZE,
};
use super::discover::{self, WalFileData};
use super::factory::WalParser;
use super::scan::parse_wal_file_bytes;
use super::types::{ParsedWalEntry, WalEntryIter};
pub struct LocalWalParser {
wal_dir: Option<PathBuf>,
all_entries: Vec<ParsedWalEntry>,
last_timestamp: Timestamp,
last_lsn: Lsn,
file_headers: Vec<WalFileHeader>,
recovery_mode: WalRecoveryMode,
verify_checksum: bool,
corrupted_count: usize,
skipped_count: usize,
}
impl LocalWalParser {
pub fn new() -> Self {
Self {
wal_dir: None,
all_entries: Vec::new(),
last_timestamp: 0,
last_lsn: Lsn::ZERO,
file_headers: Vec::new(),
recovery_mode: WalRecoveryMode::default(),
verify_checksum: true,
corrupted_count: 0,
skipped_count: 0,
}
}
pub fn with_recovery_mode(recovery_mode: WalRecoveryMode) -> Self {
Self {
recovery_mode,
verify_checksum: true,
..Self::new()
}
}
pub fn with_verify_checksum(mut self, verify: bool) -> Self {
self.verify_checksum = verify;
self
}
pub fn corrupted_count(&self) -> usize {
self.corrupted_count
}
pub fn skipped_count(&self) -> usize {
self.skipped_count
}
pub fn file_headers(&self) -> &[WalFileHeader] {
&self.file_headers
}
fn parse_wal_files(&mut self, wal_dir: &Path) -> WalResult<()> {
let wal_files = discover::discover_wal_files(wal_dir, self.recovery_mode)?;
for path in wal_files {
if let Err(e) = self.parse_wal_file(&path) {
match self.recovery_mode {
WalRecoveryMode::AbortOnCorruption => {
return Err(WalError::RecoveryAborted(format!(
"Failed to parse {}: {}",
path.display(),
e
)));
}
_ => {
self.corrupted_count += 1;
continue;
}
}
}
}
self.all_entries.sort_by_key(|e| e.lsn);
Ok(())
}
fn parse_wal_file(&mut self, path: &Path) -> WalResult<()> {
let Some(data) = discover::read_wal_file(path, self.verify_checksum)? else {
return Ok(());
};
log_file_debug(path, &data);
self.file_headers.push(data.file_header);
let result = parse_wal_file_bytes(
&data.buffer,
data.file_header.start_lsn(),
self.recovery_mode,
self.verify_checksum,
)?;
self.all_entries.extend(result.all_entries);
self.corrupted_count += result.corrupted_count;
self.skipped_count += result.skipped_count;
self.last_timestamp = self.last_timestamp.max(result.last_timestamp);
if result.last_lsn > self.last_lsn {
self.last_lsn = result.last_lsn;
}
Ok(())
}
pub fn iter_entries(&self) -> WalEntryIter<'_> {
WalEntryIter::new(&self.all_entries)
}
pub fn parse_all_entries(&self) -> Vec<ParsedWalEntry> {
self.all_entries.clone()
}
pub fn last_lsn(&self) -> Lsn {
self.last_lsn
}
pub fn get_entry_by_lsn(&self, lsn: Lsn) -> Option<&ParsedWalEntry> {
self.all_entries.iter().find(|e| e.lsn == lsn)
}
pub fn get_entries_in_lsn_range(&self, start: Lsn, end: Lsn) -> Vec<&ParsedWalEntry> {
self.all_entries
.iter()
.filter(|e| e.lsn >= start && e.lsn <= end)
.collect()
}
pub fn get_all_entries_sorted_by_lsn(&self) -> Vec<&ParsedWalEntry> {
let mut entries: Vec<_> = self.all_entries.iter().collect();
entries.sort_by_key(|e| e.lsn);
entries
}
}
impl Default for LocalWalParser {
fn default() -> Self {
Self::new()
}
}
impl WalParser for LocalWalParser {
fn open(&mut self, wal_uri: &str) -> WalResult<()> {
let wal_dir = PathBuf::from(wal_uri);
self.wal_dir = Some(wal_dir.clone());
self.parse_wal_files(&wal_dir)
}
fn close(&mut self) {
self.all_entries.clear();
self.file_headers.clear();
self.last_timestamp = 0;
self.last_lsn = Lsn::ZERO;
self.corrupted_count = 0;
self.skipped_count = 0;
}
fn last_timestamp(&self) -> Timestamp {
self.last_timestamp
}
}
fn log_file_debug(path: &Path, data: &WalFileData) {
let file_start_lsn = data.file_header.start_lsn();
log::error!(
"WAL FILE DEBUG: path={:?}, file_size={}, file_start_lsn={}, checkpoint_seq={}, thread_id={}, first_record_at_64={:?}",
path, data.buffer.len(), file_start_lsn, data.file_header.checkpoint_seq, data.file_header.thread_id,
if data.buffer.len() >= WAL_FILE_HEADER_SIZE + WAL_HEADER_SIZE {
WalHeader::from_bytes(&data.buffer[WAL_FILE_HEADER_SIZE..WAL_FILE_HEADER_SIZE + WAL_HEADER_SIZE])
.map(|h| format!("prev_lsn={}, lsn={}, ts={}, len={}", h.prev_lsn(), h.lsn(), h.timestamp, h.length()))
.unwrap_or_else(|| "invalid header".to_string())
} else {
"no records".to_string()
}
);
}