reddb_server/storage/engine/
database.rs1use std::io;
44use std::path::{Path, PathBuf};
45use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
46
47use super::{Page, PageType, Pager, PagerConfig};
48use crate::storage::transaction::snapshot::SnapshotManager;
49use crate::storage::wal::{
50 CheckpointError, CheckpointMode, CheckpointResult, Checkpointer, Transaction,
51 TransactionManager, TxError,
52};
53
54#[derive(Debug, Clone)]
56pub struct DatabaseConfig {
57 pub cache_size: usize,
59 pub read_only: bool,
61 pub create: bool,
63 pub checkpoint_mode: CheckpointMode,
65 pub auto_checkpoint_threshold: u32,
68 pub verify_checksums: bool,
70}
71
72impl Default for DatabaseConfig {
73 fn default() -> Self {
74 Self {
75 cache_size: 10_000,
76 read_only: false,
77 create: true,
78 checkpoint_mode: CheckpointMode::Full,
79 auto_checkpoint_threshold: 1000,
80 verify_checksums: true,
81 }
82 }
83}
84
85#[derive(Debug)]
87pub enum DatabaseError {
88 Io(io::Error),
90 Pager(String),
92 LockPoisoned(&'static str),
94 Transaction(TxError),
96 Checkpoint(CheckpointError),
98 ReadOnly,
100 Closed,
102}
103
104impl std::fmt::Display for DatabaseError {
105 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106 match self {
107 Self::Io(e) => write!(f, "I/O error: {}", e),
108 Self::Pager(msg) => write!(f, "Pager error: {}", msg),
109 Self::LockPoisoned(name) => write!(f, "Lock poisoned: {}", name),
110 Self::Transaction(e) => write!(f, "Transaction error: {}", e),
111 Self::Checkpoint(e) => write!(f, "Checkpoint error: {}", e),
112 Self::ReadOnly => write!(f, "Database is read-only"),
113 Self::Closed => write!(f, "Database is closed"),
114 }
115 }
116}
117
118impl std::error::Error for DatabaseError {}
119
120impl From<io::Error> for DatabaseError {
121 fn from(e: io::Error) -> Self {
122 Self::Io(e)
123 }
124}
125
126impl From<TxError> for DatabaseError {
127 fn from(e: TxError) -> Self {
128 Self::Transaction(e)
129 }
130}
131
132impl From<CheckpointError> for DatabaseError {
133 fn from(e: CheckpointError) -> Self {
134 Self::Checkpoint(e)
135 }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140enum DbState {
141 Open,
142 Closed,
143}
144
145pub struct Database {
149 path: PathBuf,
151 wal_path: PathBuf,
153 pager: Arc<Pager>,
155 tx_manager: Arc<TransactionManager>,
157 config: DatabaseConfig,
159 state: RwLock<DbState>,
161 pages_since_checkpoint: RwLock<u32>,
163 #[allow(dead_code)]
167 bgwriter: Option<crate::storage::cache::bgwriter::BgWriterHandle>,
168}
169
170impl Database {
171 fn state_read(&self) -> Result<RwLockReadGuard<'_, DbState>, DatabaseError> {
172 self.state
173 .read()
174 .map_err(|_| DatabaseError::LockPoisoned("database state"))
175 }
176
177 fn state_write(&self) -> Result<RwLockWriteGuard<'_, DbState>, DatabaseError> {
178 self.state
179 .write()
180 .map_err(|_| DatabaseError::LockPoisoned("database state"))
181 }
182
183 fn pages_since_checkpoint_read(&self) -> Result<RwLockReadGuard<'_, u32>, DatabaseError> {
184 self.pages_since_checkpoint
185 .read()
186 .map_err(|_| DatabaseError::LockPoisoned("pages since checkpoint"))
187 }
188
189 fn pages_since_checkpoint_write(&self) -> Result<RwLockWriteGuard<'_, u32>, DatabaseError> {
190 self.pages_since_checkpoint
191 .write()
192 .map_err(|_| DatabaseError::LockPoisoned("pages since checkpoint"))
193 }
194
195 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, DatabaseError> {
197 Self::open_with_config(path, DatabaseConfig::default())
198 }
199
200 pub fn bgwriter_stats(&self) -> Option<crate::storage::cache::bgwriter::BgWriterStatsSnapshot> {
204 self.bgwriter.as_ref().map(|h| h.stats.snapshot())
205 }
206
207 pub fn open_with_config<P: AsRef<Path>>(
209 path: P,
210 config: DatabaseConfig,
211 ) -> Result<Self, DatabaseError> {
212 let path = path.as_ref().to_path_buf();
213 let wal_path = reddb_file::layout::engine_wal_path(&path);
214
215 let pager_config = PagerConfig {
217 cache_size: config.cache_size,
218 read_only: config.read_only,
219 create: config.create,
220 verify_checksums: config.verify_checksums,
221 double_write: true,
222 encryption: None,
223 };
224
225 let pager =
227 Pager::open(&path, pager_config).map_err(|e| DatabaseError::Pager(e.to_string()))?;
228 let pager = Arc::new(pager);
229 let snapshot_manager = Arc::new(SnapshotManager::new());
230
231 if wal_path.exists() && !config.read_only {
233 let recovery_result = Checkpointer::recover(&pager, &wal_path)?;
234 snapshot_manager.observe_committed_xid(recovery_result.max_transaction_id);
235 if recovery_result.pages_checkpointed > 0 {
236 tracing::info!(
237 transactions = recovery_result.transactions_processed,
238 pages = recovery_result.pages_checkpointed,
239 "WAL recovery applied"
240 );
241 }
242 }
243
244 let tx_manager = Arc::new(
246 TransactionManager::new_with_snapshot_manager(
247 Arc::clone(&pager),
248 &wal_path,
249 snapshot_manager,
250 )
251 .map_err(DatabaseError::Io)?,
252 );
253
254 let bgwriter = if config.read_only
264 || !matches!(
265 std::env::var("REDDB_BGWRITER").ok().as_deref(),
266 Some("1") | Some("true") | Some("on")
267 ) {
268 None
269 } else {
270 let flusher = std::sync::Arc::new(
271 crate::storage::cache::bgwriter::PagerDirtyFlusher::new(Arc::downgrade(&pager)),
272 );
273 Some(crate::storage::cache::bgwriter::spawn(
274 flusher,
275 crate::storage::cache::bgwriter::BgWriterConfig::default(),
276 ))
277 };
278
279 Ok(Self {
280 path,
281 wal_path,
282 pager,
283 tx_manager,
284 config,
285 state: RwLock::new(DbState::Open),
286 pages_since_checkpoint: RwLock::new(0),
287 bgwriter,
288 })
289 }
290
291 fn check_open(&self) -> Result<(), DatabaseError> {
293 if *self.state_read()? == DbState::Closed {
294 return Err(DatabaseError::Closed);
295 }
296 Ok(())
297 }
298
299 pub fn begin(&self) -> Result<Transaction, DatabaseError> {
301 self.check_open()?;
302 Ok(self.tx_manager.begin()?)
303 }
304
305 pub fn pager(&self) -> &Arc<Pager> {
307 &self.pager
308 }
309
310 pub fn tx_manager(&self) -> &Arc<TransactionManager> {
312 &self.tx_manager
313 }
314
315 pub fn allocate_page(&self, page_type: PageType) -> Result<Page, DatabaseError> {
317 self.check_open()?;
318 if self.config.read_only {
319 return Err(DatabaseError::ReadOnly);
320 }
321 self.pager
322 .allocate_page(page_type)
323 .map_err(|e| DatabaseError::Pager(e.to_string()))
324 }
325
326 pub fn read_page(&self, page_id: u32) -> Result<Page, DatabaseError> {
328 self.check_open()?;
329 self.pager
330 .read_page(page_id)
331 .map_err(|e| DatabaseError::Pager(e.to_string()))
332 }
333
334 pub fn checkpoint(&self) -> Result<CheckpointResult, DatabaseError> {
336 self.check_open()?;
337 if self.config.read_only {
338 return Err(DatabaseError::ReadOnly);
339 }
340
341 let checkpointer = Checkpointer::new(self.config.checkpoint_mode);
342 let result = checkpointer.checkpoint(&self.pager, &self.wal_path)?;
343
344 *self.pages_since_checkpoint_write()? = 0;
346
347 Ok(result)
348 }
349
350 pub fn maybe_auto_checkpoint(&self) -> Result<Option<CheckpointResult>, DatabaseError> {
352 if self.config.auto_checkpoint_threshold == 0 {
353 return Ok(None);
354 }
355
356 let pages = *self.pages_since_checkpoint_read()?;
357 if pages >= self.config.auto_checkpoint_threshold {
358 Ok(Some(self.checkpoint()?))
359 } else {
360 Ok(None)
361 }
362 }
363
364 pub fn increment_page_count(&self, count: u32) {
366 let mut pages = self
367 .pages_since_checkpoint
368 .write()
369 .unwrap_or_else(|poisoned| poisoned.into_inner());
370 *pages = pages.saturating_add(count);
371 }
372
373 pub fn sync(&self) -> Result<(), DatabaseError> {
375 self.check_open()?;
376 self.pager
377 .sync()
378 .map_err(|e| DatabaseError::Pager(e.to_string()))?;
379 self.tx_manager.sync_wal()?;
380 Ok(())
381 }
382
383 pub fn close(self) -> Result<(), DatabaseError> {
387 *self.state_write()? = DbState::Closed;
389
390 if self.tx_manager.has_active_transactions() {
392 tracing::warn!("closing database with active transactions");
393 }
394
395 if !self.config.read_only {
397 let checkpointer = Checkpointer::new(CheckpointMode::Truncate);
398 let _ = checkpointer.checkpoint(&self.pager, &self.wal_path);
399 }
400
401 let _ = self.pager.sync();
403
404 Ok(())
405 }
406
407 pub fn path(&self) -> &Path {
409 &self.path
410 }
411
412 pub fn wal_path(&self) -> &Path {
414 &self.wal_path
415 }
416
417 pub fn is_read_only(&self) -> bool {
419 self.config.read_only
420 }
421
422 pub fn page_count(&self) -> u32 {
424 self.pager.page_count().unwrap_or(0)
425 }
426
427 pub fn file_size(&self) -> Result<u64, DatabaseError> {
429 self.pager
430 .file_size()
431 .map_err(|e| DatabaseError::Pager(e.to_string()))
432 }
433
434 pub fn cache_stats(&self) -> super::page_cache::CacheStats {
436 self.pager.cache_stats()
437 }
438}
439
440impl Drop for Database {
441 fn drop(&mut self) {
442 let state = self
444 .state
445 .read()
446 .unwrap_or_else(|poisoned| poisoned.into_inner());
447 if *state == DbState::Open {
448 drop(state);
449 let mut state = self
450 .state
451 .write()
452 .unwrap_or_else(|poisoned| poisoned.into_inner());
453 *state = DbState::Closed;
454 drop(state);
455
456 if !self.config.read_only {
458 let checkpointer = Checkpointer::new(CheckpointMode::Full);
459 let _ = checkpointer.checkpoint(&self.pager, &self.wal_path);
460 }
461 let _ = self.pager.sync();
462 }
463 }
464}
465
466#[cfg(test)]
467mod tests {
468 use super::*;
469 use std::fs;
470 use std::time::{SystemTime, UNIX_EPOCH};
471
472 struct TempDbPath(PathBuf);
473
474 impl std::ops::Deref for TempDbPath {
475 type Target = PathBuf;
476
477 fn deref(&self) -> &PathBuf {
478 &self.0
479 }
480 }
481
482 impl AsRef<Path> for TempDbPath {
483 fn as_ref(&self) -> &Path {
484 &self.0
485 }
486 }
487
488 impl Drop for TempDbPath {
489 fn drop(&mut self) {
490 cleanup(&self.0);
491 }
492 }
493
494 fn temp_db_path() -> TempDbPath {
495 let timestamp = SystemTime::now()
496 .duration_since(UNIX_EPOCH)
497 .unwrap()
498 .as_nanos();
499 TempDbPath(std::env::temp_dir().join(format!("reddb_engine_test_{}.rdb", timestamp)))
500 }
501
502 fn cleanup(path: &Path) {
503 let _ = fs::remove_file(path);
504 let wal_path = reddb_file::layout::engine_wal_path(path);
505 let _ = fs::remove_file(wal_path);
506 for sidecar in reddb_file::layout::pager_shadow_sidecar_paths(path) {
507 let _ = fs::remove_file(sidecar);
508 }
509 }
510
511 #[test]
512 fn test_database_open_create() {
513 let path = temp_db_path();
514 cleanup(&path);
515
516 {
517 let db = Database::open(&path).unwrap();
518 assert!(!db.is_read_only());
519 assert_eq!(db.page_count(), 67);
522 }
523
524 {
526 let db = Database::open(&path).unwrap();
527 assert_eq!(db.page_count(), 67);
528 }
529
530 cleanup(&path);
531 }
532
533 #[test]
534 fn test_database_transaction() {
535 let path = temp_db_path();
536 cleanup(&path);
537
538 {
539 let db = Database::open(&path).unwrap();
540
541 let page = db.allocate_page(PageType::BTreeLeaf).unwrap();
543 let page_id = page.page_id();
544
545 let mut tx = db.begin().unwrap();
547
548 let mut page = Page::new(PageType::BTreeLeaf, page_id);
550 page.as_bytes_mut()[100] = 0xAB;
551 tx.write_page(page_id, page).unwrap();
552
553 tx.commit().unwrap();
555
556 let read_page = db.read_page(page_id).unwrap();
558 assert_eq!(read_page.as_bytes()[100], 0xAB);
559 }
560
561 cleanup(&path);
562 }
563
564 #[test]
565 fn test_database_crash_recovery() {
566 let path = temp_db_path();
567 cleanup(&path);
568
569 let page_id;
570
571 {
573 let db = Database::open(&path).unwrap();
574
575 let page = db.allocate_page(PageType::BTreeLeaf).unwrap();
577 page_id = page.page_id();
578
579 let mut tx = db.begin().unwrap();
581 let mut page = Page::new(PageType::BTreeLeaf, page_id);
582 page.as_bytes_mut()[100] = 0xCD;
583 tx.write_page(page_id, page).unwrap();
584 tx.commit().unwrap();
585
586 db.sync().unwrap();
588
589 }
591
592 {
594 let db = Database::open(&path).unwrap();
595
596 let read_page = db.read_page(page_id).unwrap();
598 assert_eq!(read_page.as_bytes()[100], 0xCD);
599 }
600
601 cleanup(&path);
602 }
603
604 #[test]
605 fn test_database_checkpoint() {
606 let path = temp_db_path();
607 cleanup(&path);
608
609 {
610 let db = Database::open(&path).unwrap();
611
612 let page1 = db.allocate_page(PageType::BTreeLeaf).unwrap();
614 let page2 = db.allocate_page(PageType::BTreeLeaf).unwrap();
615
616 let mut tx1 = db.begin().unwrap();
618 let mut p1 = Page::new(PageType::BTreeLeaf, page1.page_id());
619 p1.as_bytes_mut()[100] = 0x11;
620 tx1.write_page(page1.page_id(), p1).unwrap();
621 tx1.commit().unwrap();
622
623 let mut tx2 = db.begin().unwrap();
624 let mut p2 = Page::new(PageType::BTreeLeaf, page2.page_id());
625 p2.as_bytes_mut()[100] = 0x22;
626 tx2.write_page(page2.page_id(), p2).unwrap();
627 tx2.commit().unwrap();
628
629 let result = db.checkpoint().unwrap();
631 assert_eq!(result.transactions_processed, 2);
632 assert!(result.pages_checkpointed >= 2);
633
634 db.close().unwrap();
636 }
637
638 {
640 let db = Database::open(&path).unwrap();
641 assert!(db.page_count() >= 3); }
644
645 cleanup(&path);
646 }
647
648 #[test]
649 fn test_database_read_only() {
650 let path = temp_db_path();
651 cleanup(&path);
652
653 {
655 let db = Database::open(&path).unwrap();
656 let page = db.allocate_page(PageType::BTreeLeaf).unwrap();
657 db.close().unwrap();
658 }
659
660 {
662 let config = DatabaseConfig {
663 read_only: true,
664 ..Default::default()
665 };
666 let db = Database::open_with_config(&path, config).unwrap();
667 assert!(db.is_read_only());
668
669 assert!(db.allocate_page(PageType::BTreeLeaf).is_err());
671 }
672
673 cleanup(&path);
674 }
675
676 #[test]
677 fn test_database_multiple_transactions() {
678 let path = temp_db_path();
679 cleanup(&path);
680
681 {
682 let db = Database::open(&path).unwrap();
683
684 let page1 = db.allocate_page(PageType::BTreeLeaf).unwrap();
686 let page2 = db.allocate_page(PageType::BTreeLeaf).unwrap();
687
688 let mut tx1 = db.begin().unwrap();
690 let mut tx2 = db.begin().unwrap();
691
692 let mut p1 = Page::new(PageType::BTreeLeaf, page1.page_id());
694 p1.as_bytes_mut()[100] = 0x11;
695 tx1.write_page(page1.page_id(), p1).unwrap();
696
697 let mut p2 = Page::new(PageType::BTreeLeaf, page2.page_id());
699 p2.as_bytes_mut()[100] = 0x22;
700 tx2.write_page(page2.page_id(), p2).unwrap();
701
702 tx1.commit().unwrap();
704 tx2.commit().unwrap();
705
706 let r1 = db.read_page(page1.page_id()).unwrap();
708 let r2 = db.read_page(page2.page_id()).unwrap();
709 assert_eq!(r1.as_bytes()[100], 0x11);
710 assert_eq!(r2.as_bytes()[100], 0x22);
711 }
712
713 cleanup(&path);
714 }
715
716 #[test]
717 fn test_begin_returns_structured_error_when_state_lock_is_poisoned() {
718 let path = temp_db_path();
719 cleanup(&path);
720
721 {
722 let db = Arc::new(Database::open(&path).unwrap());
723 let poison_target = Arc::clone(&db);
724 let _ = std::thread::spawn(move || {
725 let _guard = poison_target
726 .state
727 .write()
728 .expect("state lock should be acquired");
729 panic!("poison database state");
730 })
731 .join();
732
733 match db.begin() {
734 Ok(_) => panic!("begin should fail after state lock poisoning"),
735 Err(err) => {
736 assert!(matches!(err, DatabaseError::LockPoisoned("database state")))
737 }
738 }
739 }
740
741 cleanup(&path);
742 }
743}