use std::{
fs::{self, File},
io::{BufReader, BufWriter, Read, Write},
path::{Path, PathBuf},
};
use crate::{
persistence::binary::io::{read_u32, read_u64, write_u32, write_u64},
wal::{entry::Lsn, writer::verify_checksum},
StorageError,
};
pub const CHECKPOINT_MAGIC: &[u8; 4] = b"VCHK";
pub const CHECKPOINT_VERSION: u32 = 1;
pub const CHECKPOINT_HEADER_SIZE: usize = 32;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CheckpointHeader {
pub version: u32,
pub lsn: Lsn,
pub timestamp_ms: u64,
pub num_tables: u32,
pub checksum: u32,
}
impl CheckpointHeader {
pub fn new(lsn: Lsn, timestamp_ms: u64, num_tables: u32, checksum: u32) -> Self {
Self { version: CHECKPOINT_VERSION, lsn, timestamp_ms, num_tables, checksum }
}
pub fn write<W: Write>(&self, writer: &mut W) -> Result<(), StorageError> {
writer.write_all(CHECKPOINT_MAGIC).map_err(|e| {
StorageError::IoError(format!("Failed to write checkpoint magic: {}", e))
})?;
write_u32(writer, self.version)?;
write_u64(writer, self.lsn)?;
write_u64(writer, self.timestamp_ms)?;
write_u32(writer, self.num_tables)?;
write_u32(writer, self.checksum)?;
Ok(())
}
pub fn read<R: Read>(reader: &mut R) -> Result<Self, StorageError> {
let mut magic = [0u8; 4];
reader.read_exact(&mut magic).map_err(|e| {
StorageError::IoError(format!("Failed to read checkpoint magic: {}", e))
})?;
if &magic != CHECKPOINT_MAGIC {
return Err(StorageError::IoError(format!(
"Invalid checkpoint file: expected magic 'VCHK', got '{}'",
String::from_utf8_lossy(&magic)
)));
}
let version = read_u32(reader)?;
if version > CHECKPOINT_VERSION {
return Err(StorageError::IoError(format!(
"Unsupported checkpoint version: {} (current: {})",
version, CHECKPOINT_VERSION
)));
}
let lsn = read_u64(reader)?;
let timestamp_ms = read_u64(reader)?;
let num_tables = read_u32(reader)?;
let checksum = read_u32(reader)?;
Ok(Self { version, lsn, timestamp_ms, num_tables, checksum })
}
}
#[derive(Debug, Clone)]
pub struct CheckpointInfo {
pub path: PathBuf,
pub lsn: Lsn,
pub timestamp_ms: u64,
pub num_tables: u32,
pub file_size: u64,
}
pub struct CheckpointWriter {
checkpoint_dir: PathBuf,
next_checkpoint_id: u64,
}
impl CheckpointWriter {
pub fn new<P: AsRef<Path>>(checkpoint_dir: P) -> Result<Self, StorageError> {
let checkpoint_dir = checkpoint_dir.as_ref().to_path_buf();
fs::create_dir_all(&checkpoint_dir).map_err(|e| {
StorageError::IoError(format!("Failed to create checkpoint dir: {}", e))
})?;
let next_checkpoint_id = Self::find_next_checkpoint_id(&checkpoint_dir)?;
Ok(Self { checkpoint_dir, next_checkpoint_id })
}
fn find_next_checkpoint_id(dir: &Path) -> Result<u64, StorageError> {
let mut max_id = 0u64;
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if let Some(id_str) =
name.strip_prefix("checkpoint_").and_then(|s| s.strip_suffix(".vchk"))
{
if let Ok(id) = id_str.parse::<u64>() {
max_id = max_id.max(id);
}
}
}
}
}
Ok(max_id + 1)
}
pub fn create_checkpoint(
&mut self,
lsn: Lsn,
data: &[u8],
num_tables: u32,
) -> Result<CheckpointInfo, StorageError> {
let checkpoint_id = self.next_checkpoint_id;
self.next_checkpoint_id += 1;
let timestamp_ms = current_timestamp_ms();
let checksum = crc32(data);
let temp_path = self.checkpoint_dir.join(format!("checkpoint_{}.tmp", checkpoint_id));
let final_path = self.checkpoint_dir.join(format!("checkpoint_{}.vchk", checkpoint_id));
{
let file = File::create(&temp_path).map_err(|e| {
StorageError::IoError(format!("Failed to create temp checkpoint: {}", e))
})?;
let mut writer = BufWriter::new(file);
let header = CheckpointHeader::new(lsn, timestamp_ms, num_tables, checksum);
header.write(&mut writer)?;
writer.write_all(data).map_err(|e| {
StorageError::IoError(format!("Failed to write checkpoint data: {}", e))
})?;
writer
.flush()
.map_err(|e| StorageError::IoError(format!("Failed to flush checkpoint: {}", e)))?;
}
fs::rename(&temp_path, &final_path)
.map_err(|e| StorageError::IoError(format!("Failed to finalize checkpoint: {}", e)))?;
let file_size = fs::metadata(&final_path)
.map_err(|e| StorageError::IoError(format!("Failed to get checkpoint size: {}", e)))?
.len();
log::info!(
"Created checkpoint {} at LSN {} ({} tables, {} bytes)",
checkpoint_id,
lsn,
num_tables,
file_size
);
Ok(CheckpointInfo { path: final_path, lsn, timestamp_ms, num_tables, file_size })
}
pub fn checkpoint_dir(&self) -> &Path {
&self.checkpoint_dir
}
pub fn list_checkpoints(&self) -> Result<Vec<CheckpointInfo>, StorageError> {
let mut checkpoints = Vec::new();
if let Ok(entries) = fs::read_dir(&self.checkpoint_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "vchk") {
if let Ok(info) = Self::read_checkpoint_info(&path) {
checkpoints.push(info);
}
}
}
}
checkpoints.sort_by_key(|c| c.lsn);
Ok(checkpoints)
}
pub fn read_checkpoint_info(path: &Path) -> Result<CheckpointInfo, StorageError> {
let file = File::open(path)
.map_err(|e| StorageError::IoError(format!("Failed to open checkpoint: {}", e)))?;
let mut reader = BufReader::new(file);
let header = CheckpointHeader::read(&mut reader)?;
let file_size = fs::metadata(path)
.map_err(|e| StorageError::IoError(format!("Failed to get checkpoint size: {}", e)))?
.len();
Ok(CheckpointInfo {
path: path.to_path_buf(),
lsn: header.lsn,
timestamp_ms: header.timestamp_ms,
num_tables: header.num_tables,
file_size,
})
}
pub fn latest_checkpoint(&self) -> Result<Option<CheckpointInfo>, StorageError> {
let checkpoints = self.list_checkpoints()?;
Ok(checkpoints.into_iter().last())
}
pub fn cleanup_old_checkpoints(&self, keep_count: usize) -> Result<usize, StorageError> {
let checkpoints = self.list_checkpoints()?;
let mut removed = 0;
if checkpoints.len() > keep_count {
let to_remove = checkpoints.len() - keep_count;
for checkpoint in checkpoints.into_iter().take(to_remove) {
if fs::remove_file(&checkpoint.path).is_ok() {
log::debug!("Removed old checkpoint: {:?}", checkpoint.path);
removed += 1;
}
}
}
Ok(removed)
}
}
pub fn read_checkpoint_data(path: &Path) -> Result<(CheckpointHeader, Vec<u8>), StorageError> {
let file = File::open(path)
.map_err(|e| StorageError::IoError(format!("Failed to open checkpoint: {}", e)))?;
let mut reader = BufReader::new(file);
let header = CheckpointHeader::read(&mut reader)?;
let mut data = Vec::new();
reader
.read_to_end(&mut data)
.map_err(|e| StorageError::IoError(format!("Failed to read checkpoint data: {}", e)))?;
if !verify_checksum(&data, header.checksum) {
return Err(StorageError::IoError(
"Checkpoint data checksum mismatch - file may be corrupted".to_string(),
));
}
Ok((header, data))
}
fn crc32(data: &[u8]) -> u32 {
const CRC32_TABLE: [u32; 256] = {
let mut table = [0u32; 256];
let mut i = 0;
while i < 256 {
let mut crc = i as u32;
let mut j = 0;
while j < 8 {
if crc & 1 != 0 {
crc = (crc >> 1) ^ 0xEDB88320;
} else {
crc >>= 1;
}
j += 1;
}
table[i] = crc;
i += 1;
}
table
};
let mut crc = 0xFFFFFFFF;
for &byte in data {
let index = ((crc ^ byte as u32) & 0xFF) as usize;
crc = (crc >> 8) ^ CRC32_TABLE[index];
}
crc ^ 0xFFFFFFFF
}
fn current_timestamp_ms() -> u64 {
use instant::SystemTime;
SystemTime::now()
.duration_since(instant::SystemTime::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use tempfile::TempDir;
use super::*;
#[test]
fn test_checkpoint_header_roundtrip() {
let header = CheckpointHeader::new(100, 1234567890, 5, 0xDEADBEEF);
let mut buf = Vec::new();
header.write(&mut buf).unwrap();
assert_eq!(buf.len(), CHECKPOINT_HEADER_SIZE);
let mut reader = &buf[..];
let decoded = CheckpointHeader::read(&mut reader).unwrap();
assert_eq!(header, decoded);
}
#[test]
fn test_checkpoint_header_invalid_magic() {
let mut buf = [0u8; CHECKPOINT_HEADER_SIZE];
buf[0..4].copy_from_slice(b"XXXX");
let mut reader = &buf[..];
let result = CheckpointHeader::read(&mut reader);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Invalid checkpoint file"));
}
#[test]
fn test_checkpoint_writer_create() {
let temp_dir = TempDir::new().unwrap();
let writer = CheckpointWriter::new(temp_dir.path()).unwrap();
assert!(writer.checkpoint_dir().exists());
}
#[test]
fn test_checkpoint_writer_create_checkpoint() {
let temp_dir = TempDir::new().unwrap();
let mut writer = CheckpointWriter::new(temp_dir.path()).unwrap();
let data = b"test checkpoint data";
let info = writer.create_checkpoint(100, data, 3).unwrap();
assert_eq!(info.lsn, 100);
assert_eq!(info.num_tables, 3);
assert!(info.path.exists());
assert!(info.file_size > CHECKPOINT_HEADER_SIZE as u64);
}
#[test]
fn test_checkpoint_writer_list_checkpoints() {
let temp_dir = TempDir::new().unwrap();
let mut writer = CheckpointWriter::new(temp_dir.path()).unwrap();
writer.create_checkpoint(10, b"data1", 1).unwrap();
writer.create_checkpoint(20, b"data2", 2).unwrap();
writer.create_checkpoint(30, b"data3", 3).unwrap();
let checkpoints = writer.list_checkpoints().unwrap();
assert_eq!(checkpoints.len(), 3);
assert_eq!(checkpoints[0].lsn, 10);
assert_eq!(checkpoints[1].lsn, 20);
assert_eq!(checkpoints[2].lsn, 30);
}
#[test]
fn test_checkpoint_writer_latest() {
let temp_dir = TempDir::new().unwrap();
let mut writer = CheckpointWriter::new(temp_dir.path()).unwrap();
assert!(writer.latest_checkpoint().unwrap().is_none());
writer.create_checkpoint(10, b"data1", 1).unwrap();
writer.create_checkpoint(20, b"data2", 2).unwrap();
let latest = writer.latest_checkpoint().unwrap().unwrap();
assert_eq!(latest.lsn, 20);
}
#[test]
fn test_checkpoint_writer_cleanup() {
let temp_dir = TempDir::new().unwrap();
let mut writer = CheckpointWriter::new(temp_dir.path()).unwrap();
for i in 1..=5 {
writer.create_checkpoint(i * 10, format!("data{}", i).as_bytes(), 1).unwrap();
}
assert_eq!(writer.list_checkpoints().unwrap().len(), 5);
let removed = writer.cleanup_old_checkpoints(2).unwrap();
assert_eq!(removed, 3);
assert_eq!(writer.list_checkpoints().unwrap().len(), 2);
let remaining = writer.list_checkpoints().unwrap();
assert_eq!(remaining[0].lsn, 40);
assert_eq!(remaining[1].lsn, 50);
}
#[test]
fn test_read_checkpoint_data() {
let temp_dir = TempDir::new().unwrap();
let mut writer = CheckpointWriter::new(temp_dir.path()).unwrap();
let original_data = b"test checkpoint data for verification";
let info = writer.create_checkpoint(100, original_data, 5).unwrap();
let (header, data) = read_checkpoint_data(&info.path).unwrap();
assert_eq!(header.lsn, 100);
assert_eq!(header.num_tables, 5);
assert_eq!(data, original_data);
}
#[test]
fn test_crc32() {
assert_eq!(crc32(b"123456789"), 0xCBF43926);
assert_eq!(crc32(b""), 0x00000000);
}
}