reddb_server/storage/wal/
checkpoint.rs1use std::collections::{HashMap, HashSet};
22use std::io;
23use std::path::Path;
24
25use super::reader::WalReader;
26use super::record::WalRecord;
27use super::writer::WalWriter;
28use crate::storage::engine::{Page, Pager, PAGE_SIZE};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum CheckpointMode {
33 Passive,
35 Full,
37 Restart,
39 Truncate,
41}
42
43#[derive(Debug, Clone, Default)]
45pub struct CheckpointResult {
46 pub transactions_processed: u64,
48 pub max_transaction_id: u64,
50 pub pages_checkpointed: u64,
52 pub records_processed: u64,
54 pub checkpoint_lsn: u64,
56 pub wal_truncated: bool,
58}
59
60#[derive(Debug)]
62pub enum CheckpointError {
63 Io(io::Error),
65 Pager(String),
67 CorruptedWal(String),
69 NoWal,
71}
72
73impl std::fmt::Display for CheckpointError {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 match self {
76 Self::Io(e) => write!(f, "I/O error: {}", e),
77 Self::Pager(msg) => write!(f, "Pager error: {}", msg),
78 Self::CorruptedWal(msg) => write!(f, "Corrupted WAL: {}", msg),
79 Self::NoWal => write!(f, "No WAL file found"),
80 }
81 }
82}
83
84impl std::error::Error for CheckpointError {}
85
86impl From<io::Error> for CheckpointError {
87 fn from(e: io::Error) -> Self {
88 Self::Io(e)
89 }
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94enum TxState {
95 Active,
96 Committed,
97 Aborted,
98}
99
100#[derive(Debug)]
102struct PendingWrite {
103 tx_id: u64,
104 page_id: u32,
105 data: Vec<u8>,
106 lsn: u64,
107}
108
109fn result_observe_tx_id(max_transaction_id: &mut u64, tx_id: u64) {
110 *max_transaction_id = (*max_transaction_id).max(tx_id);
111}
112
113pub struct Checkpointer {
117 mode: CheckpointMode,
119}
120
121impl Checkpointer {
122 pub fn new(mode: CheckpointMode) -> Self {
124 Self { mode }
125 }
126
127 pub fn default_mode() -> Self {
129 Self::new(CheckpointMode::Full)
130 }
131
132 pub fn checkpoint(
145 &self,
146 pager: &Pager,
147 wal_path: &Path,
148 ) -> Result<CheckpointResult, CheckpointError> {
149 let wal_reader = match WalReader::open(wal_path) {
151 Ok(r) => r,
152 Err(e) if e.kind() == io::ErrorKind::NotFound => {
153 return Ok(CheckpointResult::default());
155 }
156 Err(e) => return Err(CheckpointError::Io(e)),
157 };
158
159 let mut tx_states: HashMap<u64, TxState> = HashMap::new();
161 let mut pending_writes: Vec<PendingWrite> = Vec::new();
162 let mut records_processed: u64 = 0;
163 let mut last_lsn: u64 = 0;
164 let mut max_transaction_id: u64 = 0;
165
166 for record_result in wal_reader.iter() {
167 let (lsn, record) = record_result.map_err(CheckpointError::Io)?;
168 records_processed += 1;
169 last_lsn = lsn;
170
171 match record {
172 WalRecord::PageOwnership { .. } => {
173 return Err(CheckpointError::Io(io::Error::other(
174 "page ownership journal requires recovery before checkpoint",
175 )));
176 }
177 WalRecord::Begin { tx_id } => {
178 result_observe_tx_id(&mut max_transaction_id, tx_id);
179 tx_states.insert(tx_id, TxState::Active);
180 }
181 WalRecord::Commit { tx_id } => {
182 result_observe_tx_id(&mut max_transaction_id, tx_id);
183 tx_states.insert(tx_id, TxState::Committed);
184 }
185 WalRecord::Rollback { tx_id } => {
186 result_observe_tx_id(&mut max_transaction_id, tx_id);
187 tx_states.insert(tx_id, TxState::Aborted);
188 }
189 WalRecord::PageWrite {
190 tx_id,
191 page_id,
192 data,
193 } => {
194 result_observe_tx_id(&mut max_transaction_id, tx_id);
195 pending_writes.push(PendingWrite {
196 tx_id,
197 page_id,
198 data,
199 lsn,
200 });
201 }
202 WalRecord::Checkpoint {
203 lsn: _checkpoint_lsn,
204 } => {
205 }
208 WalRecord::TxCommitBatch { tx_id, .. } => {
209 result_observe_tx_id(&mut max_transaction_id, tx_id);
210 }
213 WalRecord::VectorInsert { .. } => {
214 crate::runtime::turbo_crash_inject::fire(
218 crate::runtime::turbo_crash_inject::InjectionPoint::MidCheckpoint,
219 );
220 }
221 WalRecord::ProbabilisticDelta { .. } => {
222 }
225 WalRecord::FullPageImage { tx_id, .. } => {
226 result_observe_tx_id(&mut max_transaction_id, tx_id);
227 }
231 }
232 }
233
234 let committed_txs: HashSet<u64> = tx_states
236 .iter()
237 .filter(|(_, state)| **state == TxState::Committed)
238 .map(|(tx_id, _)| *tx_id)
239 .collect();
240
241 let mut latest_writes: HashMap<u32, Vec<u8>> = HashMap::new();
244
245 for write in pending_writes {
246 if committed_txs.contains(&write.tx_id) {
247 latest_writes.insert(write.page_id, write.data);
249 }
250 }
251
252 if !latest_writes.is_empty() {
254 pager
255 .set_checkpoint_in_progress(true, last_lsn)
256 .map_err(|e| CheckpointError::Pager(e.to_string()))?;
257 }
258
259 let mut pages_checkpointed: u64 = 0;
261
262 for (page_id, data) in &latest_writes {
263 if data.len() != PAGE_SIZE {
265 return Err(CheckpointError::CorruptedWal(format!(
266 "Page {} has wrong size: {} (expected {})",
267 page_id,
268 data.len(),
269 PAGE_SIZE
270 )));
271 }
272
273 let mut page_data = [0u8; PAGE_SIZE];
274 page_data.copy_from_slice(data);
275 let page = Page::from_bytes(page_data);
276
277 pager
279 .write_page(*page_id, page)
280 .map_err(|e| CheckpointError::Pager(e.to_string()))?;
281
282 pages_checkpointed += 1;
283 }
284
285 pager
287 .sync()
288 .map_err(|e| CheckpointError::Pager(e.to_string()))?;
289
290 if !latest_writes.is_empty() {
292 pager
293 .complete_checkpoint(last_lsn)
294 .map_err(|e| CheckpointError::Pager(e.to_string()))?;
295 }
296
297 let wal_truncated = matches!(
299 self.mode,
300 CheckpointMode::Restart | CheckpointMode::Truncate
301 );
302
303 if wal_truncated {
304 let mut wal_writer = WalWriter::open(wal_path)?;
305 wal_writer.truncate()?;
306
307 let checkpoint_record = WalRecord::Checkpoint { lsn: last_lsn };
309 wal_writer.append(&checkpoint_record)?;
310 wal_writer.sync()?;
311 }
312
313 Ok(CheckpointResult {
314 transactions_processed: committed_txs.len() as u64,
315 max_transaction_id,
316 pages_checkpointed,
317 records_processed,
318 checkpoint_lsn: last_lsn,
319 wal_truncated,
320 })
321 }
322
323 pub fn recover(pager: &Pager, wal_path: &Path) -> Result<CheckpointResult, CheckpointError> {
338 if let Ok(header) = pager.header() {
340 if header.checkpoint_in_progress {
341 let _ = pager.set_checkpoint_in_progress(false, 0);
344 }
345 }
346 let checkpointer = Self::new(CheckpointMode::Truncate);
347 checkpointer.checkpoint(pager, wal_path)
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354 use crate::storage::engine::PageType;
355 use std::fs;
356 use std::time::{SystemTime, UNIX_EPOCH};
357
358 fn temp_dir() -> std::path::PathBuf {
359 let timestamp = SystemTime::now()
360 .duration_since(UNIX_EPOCH)
361 .unwrap()
362 .as_nanos();
363 std::env::temp_dir().join(format!("reddb_checkpoint_test_{}", timestamp))
364 }
365
366 fn cleanup(dir: &Path) {
367 let _ = fs::remove_dir_all(dir);
368 }
369
370 fn temp_wal_path(dir: &Path, name: &str) -> std::path::PathBuf {
371 reddb_file::layout::wal_component_temp_path(dir, "checkpoint", name, std::process::id())
372 }
373
374 #[test]
375 fn test_checkpoint_empty_wal() {
376 let dir = temp_dir();
377 let _ = fs::create_dir_all(&dir);
378 let db_path = dir.join("test.db");
379 let wal_path = temp_wal_path(&dir, "empty");
380
381 let pager = Pager::open_default(&db_path).unwrap();
383
384 let checkpointer = Checkpointer::default_mode();
386 let result = checkpointer.checkpoint(&pager, &wal_path).unwrap();
387
388 assert_eq!(result.transactions_processed, 0);
389 assert_eq!(result.pages_checkpointed, 0);
390
391 cleanup(&dir);
392 }
393
394 #[test]
395 fn test_checkpoint_committed_transaction() {
396 let dir = temp_dir();
397 let _ = fs::create_dir_all(&dir);
398 let db_path = dir.join("test.db");
399 let wal_path = temp_wal_path(&dir, "committed");
400
401 let pager = Pager::open_default(&db_path).unwrap();
403
404 let page = pager.allocate_page(PageType::BTreeLeaf).unwrap();
406 let page_id = page.page_id();
407
408 {
410 let mut wal_writer = WalWriter::open(&wal_path).unwrap();
411
412 wal_writer.append(&WalRecord::Begin { tx_id: 1 }).unwrap();
414
415 let mut page_data = [0u8; PAGE_SIZE];
417 page_data[0] = 0x42; wal_writer
419 .append(&WalRecord::PageWrite {
420 tx_id: 1,
421 page_id,
422 data: page_data.to_vec(),
423 })
424 .unwrap();
425
426 wal_writer.append(&WalRecord::Commit { tx_id: 1 }).unwrap();
428
429 wal_writer.sync().unwrap();
430 }
431
432 let checkpointer = Checkpointer::new(CheckpointMode::Full);
434 let result = checkpointer.checkpoint(&pager, &wal_path).unwrap();
435
436 assert_eq!(result.transactions_processed, 1);
437 assert_eq!(result.pages_checkpointed, 1);
438 assert_eq!(result.records_processed, 3);
439
440 let read_page = pager.read_page(page_id).unwrap();
442 assert_eq!(read_page.as_bytes()[0], 0x42);
443
444 cleanup(&dir);
445 }
446
447 #[test]
448 fn test_checkpoint_aborted_transaction() {
449 let dir = temp_dir();
450 let _ = fs::create_dir_all(&dir);
451 let db_path = dir.join("test.db");
452 let wal_path = temp_wal_path(&dir, "aborted");
453
454 let pager = Pager::open_default(&db_path).unwrap();
456
457 let page = pager.allocate_page(PageType::BTreeLeaf).unwrap();
459 let page_id = page.page_id();
460
461 {
463 let mut wal_writer = WalWriter::open(&wal_path).unwrap();
464
465 wal_writer.append(&WalRecord::Begin { tx_id: 1 }).unwrap();
467
468 let mut page_data = [0u8; PAGE_SIZE];
470 page_data[0] = 0x42;
471 wal_writer
472 .append(&WalRecord::PageWrite {
473 tx_id: 1,
474 page_id,
475 data: page_data.to_vec(),
476 })
477 .unwrap();
478
479 wal_writer
481 .append(&WalRecord::Rollback { tx_id: 1 })
482 .unwrap();
483
484 wal_writer.sync().unwrap();
485 }
486
487 let checkpointer = Checkpointer::new(CheckpointMode::Full);
489 let result = checkpointer.checkpoint(&pager, &wal_path).unwrap();
490
491 assert_eq!(result.transactions_processed, 0);
493 assert_eq!(result.pages_checkpointed, 0);
494
495 let read_page = pager.read_page(page_id).unwrap();
497 assert_ne!(read_page.as_bytes()[0], 0x42);
498
499 cleanup(&dir);
500 }
501
502 #[test]
503 fn test_checkpoint_mixed_transactions() {
504 let dir = temp_dir();
505 let _ = fs::create_dir_all(&dir);
506 let db_path = dir.join("test.db");
507 let wal_path = temp_wal_path(&dir, "truncate");
508
509 let pager = Pager::open_default(&db_path).unwrap();
511
512 let page1 = pager.allocate_page(PageType::BTreeLeaf).unwrap();
514 let page2 = pager.allocate_page(PageType::BTreeLeaf).unwrap();
515 let page1_id = page1.page_id();
516 let page2_id = page2.page_id();
517
518 {
520 let mut wal_writer = WalWriter::open(&wal_path).unwrap();
521
522 wal_writer.append(&WalRecord::Begin { tx_id: 1 }).unwrap();
524 let mut page_data1 = [0u8; PAGE_SIZE];
525 page_data1[0] = 0x11;
526 wal_writer
527 .append(&WalRecord::PageWrite {
528 tx_id: 1,
529 page_id: page1_id,
530 data: page_data1.to_vec(),
531 })
532 .unwrap();
533 wal_writer.append(&WalRecord::Commit { tx_id: 1 }).unwrap();
534
535 wal_writer.append(&WalRecord::Begin { tx_id: 2 }).unwrap();
537 let mut page_data2 = [0u8; PAGE_SIZE];
538 page_data2[0] = 0x22;
539 wal_writer
540 .append(&WalRecord::PageWrite {
541 tx_id: 2,
542 page_id: page2_id,
543 data: page_data2.to_vec(),
544 })
545 .unwrap();
546 wal_writer
547 .append(&WalRecord::Rollback { tx_id: 2 })
548 .unwrap();
549
550 wal_writer.append(&WalRecord::Begin { tx_id: 3 }).unwrap();
552 let mut page_data3 = [0u8; PAGE_SIZE];
553 page_data3[0] = 0x33;
554 wal_writer
555 .append(&WalRecord::PageWrite {
556 tx_id: 3,
557 page_id: page2_id,
558 data: page_data3.to_vec(),
559 })
560 .unwrap();
561 wal_writer.append(&WalRecord::Commit { tx_id: 3 }).unwrap();
562
563 wal_writer.sync().unwrap();
564 }
565
566 let checkpointer = Checkpointer::new(CheckpointMode::Full);
568 let result = checkpointer.checkpoint(&pager, &wal_path).unwrap();
569
570 assert_eq!(result.transactions_processed, 2);
572 assert_eq!(result.max_transaction_id, 3);
573 assert_eq!(result.pages_checkpointed, 2);
574
575 let read_page1 = pager.read_page(page1_id).unwrap();
577 assert_eq!(read_page1.as_bytes()[0], 0x11);
578
579 let read_page2 = pager.read_page(page2_id).unwrap();
580 assert_eq!(read_page2.as_bytes()[0], 0x33); cleanup(&dir);
583 }
584
585 #[test]
586 fn test_checkpoint_truncate() {
587 let dir = temp_dir();
588 let _ = fs::create_dir_all(&dir);
589 let db_path = dir.join("test.db");
590 let wal_path = temp_wal_path(&dir, "full-page-images");
591
592 let pager = Pager::open_default(&db_path).unwrap();
594
595 {
597 let mut wal_writer = WalWriter::open(&wal_path).unwrap();
598 wal_writer.append(&WalRecord::Begin { tx_id: 1 }).unwrap();
599 wal_writer.append(&WalRecord::Commit { tx_id: 1 }).unwrap();
600 wal_writer.sync().unwrap();
601 }
602
603 let checkpointer = Checkpointer::new(CheckpointMode::Truncate);
605 let result = checkpointer.checkpoint(&pager, &wal_path).unwrap();
606
607 assert!(result.wal_truncated);
608
609 let records: Vec<_> = WalReader::open(&wal_path)
610 .unwrap()
611 .iter()
612 .collect::<Result<Vec<_>, _>>()
613 .unwrap();
614 assert_eq!(records.len(), 1);
615 assert_eq!(
616 records[0].1,
617 WalRecord::Checkpoint {
618 lsn: result.checkpoint_lsn
619 }
620 );
621
622 cleanup(&dir);
623 }
624}