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 disk_residency: false,
218 cache_size: config.cache_size,
219 read_only: config.read_only,
220 create: config.create,
221 verify_checksums: config.verify_checksums,
222 double_write: true,
223 encryption: None,
224 };
225
226 let pager =
228 Pager::open(&path, pager_config).map_err(|e| DatabaseError::Pager(e.to_string()))?;
229 let pager = Arc::new(pager);
230 let snapshot_manager = Arc::new(SnapshotManager::new());
231
232 if wal_path.exists() && !config.read_only {
234 let recovery_result = Checkpointer::recover(&pager, &wal_path)?;
235 snapshot_manager.observe_committed_xid(recovery_result.max_transaction_id);
236 if recovery_result.pages_checkpointed > 0 {
237 tracing::info!(
238 transactions = recovery_result.transactions_processed,
239 pages = recovery_result.pages_checkpointed,
240 "WAL recovery applied"
241 );
242 }
243 }
244
245 let tx_manager = Arc::new(
247 TransactionManager::new_with_snapshot_manager(
248 Arc::clone(&pager),
249 &wal_path,
250 snapshot_manager,
251 )
252 .map_err(DatabaseError::Io)?,
253 );
254
255 let bgwriter = if config.read_only
265 || !matches!(
266 std::env::var("REDDB_BGWRITER").ok().as_deref(),
267 Some("1") | Some("true") | Some("on")
268 ) {
269 None
270 } else {
271 let flusher = std::sync::Arc::new(
272 crate::storage::cache::bgwriter::PagerDirtyFlusher::new(Arc::downgrade(&pager)),
273 );
274 Some(crate::storage::cache::bgwriter::spawn(
275 flusher,
276 crate::storage::cache::bgwriter::BgWriterConfig::default(),
277 ))
278 };
279
280 Ok(Self {
281 path,
282 wal_path,
283 pager,
284 tx_manager,
285 config,
286 state: RwLock::new(DbState::Open),
287 pages_since_checkpoint: RwLock::new(0),
288 bgwriter,
289 })
290 }
291
292 fn check_open(&self) -> Result<(), DatabaseError> {
294 if *self.state_read()? == DbState::Closed {
295 return Err(DatabaseError::Closed);
296 }
297 Ok(())
298 }
299
300 pub fn begin(&self) -> Result<Transaction, DatabaseError> {
302 self.check_open()?;
303 Ok(self.tx_manager.begin()?)
304 }
305
306 pub fn pager(&self) -> &Arc<Pager> {
308 &self.pager
309 }
310
311 pub fn tx_manager(&self) -> &Arc<TransactionManager> {
313 &self.tx_manager
314 }
315
316 pub fn allocate_page(&self, page_type: PageType) -> Result<Page, DatabaseError> {
318 self.check_open()?;
319 if self.config.read_only {
320 return Err(DatabaseError::ReadOnly);
321 }
322 self.pager
323 .allocate_page(page_type)
324 .map_err(|e| DatabaseError::Pager(e.to_string()))
325 }
326
327 pub fn read_page(&self, page_id: u32) -> Result<Page, DatabaseError> {
329 self.check_open()?;
330 self.pager
331 .read_page(page_id)
332 .map_err(|e| DatabaseError::Pager(e.to_string()))
333 }
334
335 pub fn checkpoint(&self) -> Result<CheckpointResult, DatabaseError> {
337 self.check_open()?;
338 if self.config.read_only {
339 return Err(DatabaseError::ReadOnly);
340 }
341
342 let checkpointer = Checkpointer::new(self.config.checkpoint_mode);
343 let result = checkpointer.checkpoint(&self.pager, &self.wal_path)?;
344
345 *self.pages_since_checkpoint_write()? = 0;
347
348 Ok(result)
349 }
350
351 pub fn maybe_auto_checkpoint(&self) -> Result<Option<CheckpointResult>, DatabaseError> {
353 if self.config.auto_checkpoint_threshold == 0 {
354 return Ok(None);
355 }
356
357 let pages = *self.pages_since_checkpoint_read()?;
358 if pages >= self.config.auto_checkpoint_threshold {
359 Ok(Some(self.checkpoint()?))
360 } else {
361 Ok(None)
362 }
363 }
364
365 pub fn increment_page_count(&self, count: u32) {
367 let mut pages = self
368 .pages_since_checkpoint
369 .write()
370 .unwrap_or_else(|poisoned| poisoned.into_inner());
371 *pages = pages.saturating_add(count);
372 }
373
374 pub fn sync(&self) -> Result<(), DatabaseError> {
376 self.check_open()?;
377 self.pager
378 .sync()
379 .map_err(|e| DatabaseError::Pager(e.to_string()))?;
380 self.tx_manager.sync_wal()?;
381 Ok(())
382 }
383
384 pub fn close(self) -> Result<(), DatabaseError> {
388 *self.state_write()? = DbState::Closed;
390
391 if self.tx_manager.has_active_transactions() {
393 tracing::warn!("closing database with active transactions");
394 }
395
396 if !self.config.read_only {
398 let checkpointer = Checkpointer::new(CheckpointMode::Truncate);
399 let _ = checkpointer.checkpoint(&self.pager, &self.wal_path);
400 }
401
402 let _ = self.pager.sync();
404
405 Ok(())
406 }
407
408 pub fn path(&self) -> &Path {
410 &self.path
411 }
412
413 pub fn wal_path(&self) -> &Path {
415 &self.wal_path
416 }
417
418 pub fn is_read_only(&self) -> bool {
420 self.config.read_only
421 }
422
423 pub fn page_count(&self) -> u32 {
425 self.pager.page_count().unwrap_or(0)
426 }
427
428 pub fn file_size(&self) -> Result<u64, DatabaseError> {
430 self.pager
431 .file_size()
432 .map_err(|e| DatabaseError::Pager(e.to_string()))
433 }
434
435 pub fn cache_stats(&self) -> super::page_cache::CacheStats {
437 self.pager.cache_stats()
438 }
439}
440
441impl Drop for Database {
442 fn drop(&mut self) {
443 let state = self
445 .state
446 .read()
447 .unwrap_or_else(|poisoned| poisoned.into_inner());
448 if *state == DbState::Open {
449 drop(state);
450 let mut state = self
451 .state
452 .write()
453 .unwrap_or_else(|poisoned| poisoned.into_inner());
454 *state = DbState::Closed;
455 drop(state);
456
457 if !self.config.read_only {
459 let checkpointer = Checkpointer::new(CheckpointMode::Full);
460 let _ = checkpointer.checkpoint(&self.pager, &self.wal_path);
461 }
462 let _ = self.pager.sync();
463 }
464 }
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470 use std::fs;
471 use std::time::{SystemTime, UNIX_EPOCH};
472
473 struct TempDbPath(PathBuf);
474
475 impl std::ops::Deref for TempDbPath {
476 type Target = PathBuf;
477
478 fn deref(&self) -> &PathBuf {
479 &self.0
480 }
481 }
482
483 impl AsRef<Path> for TempDbPath {
484 fn as_ref(&self) -> &Path {
485 &self.0
486 }
487 }
488
489 impl Drop for TempDbPath {
490 fn drop(&mut self) {
491 cleanup(&self.0);
492 }
493 }
494
495 fn temp_db_path() -> TempDbPath {
496 let timestamp = SystemTime::now()
497 .duration_since(UNIX_EPOCH)
498 .unwrap()
499 .as_nanos();
500 TempDbPath(std::env::temp_dir().join(format!("reddb_engine_test_{}.rdb", timestamp)))
501 }
502
503 fn cleanup(path: &Path) {
504 let _ = fs::remove_file(path);
505 let wal_path = reddb_file::layout::engine_wal_path(path);
506 let _ = fs::remove_file(wal_path);
507 for sidecar in reddb_file::layout::pager_shadow_sidecar_paths(path) {
508 let _ = fs::remove_file(sidecar);
509 }
510 }
511
512 #[test]
513 fn test_database_open_create() {
514 let path = temp_db_path();
515 cleanup(&path);
516
517 {
518 let db = Database::open(&path).unwrap();
519 assert!(!db.is_read_only());
520 assert_eq!(db.page_count(), 67);
523 }
524
525 {
527 let db = Database::open(&path).unwrap();
528 assert_eq!(db.page_count(), 67);
529 }
530
531 cleanup(&path);
532 }
533
534 #[test]
535 fn test_database_transaction() {
536 let path = temp_db_path();
537 cleanup(&path);
538
539 {
540 let db = Database::open(&path).unwrap();
541
542 let page = db.allocate_page(PageType::BTreeLeaf).unwrap();
544 let page_id = page.page_id();
545
546 let mut tx = db.begin().unwrap();
548
549 let mut page = Page::new(PageType::BTreeLeaf, page_id);
551 page.as_bytes_mut()[100] = 0xAB;
552 tx.write_page(page_id, page).unwrap();
553
554 tx.commit().unwrap();
556
557 let read_page = db.read_page(page_id).unwrap();
559 assert_eq!(read_page.as_bytes()[100], 0xAB);
560 }
561
562 cleanup(&path);
563 }
564
565 #[test]
566 fn test_database_crash_recovery() {
567 let path = temp_db_path();
568 cleanup(&path);
569
570 let page_id;
571
572 {
574 let db = Database::open(&path).unwrap();
575
576 let page = db.allocate_page(PageType::BTreeLeaf).unwrap();
578 page_id = page.page_id();
579
580 let mut tx = db.begin().unwrap();
582 let mut page = Page::new(PageType::BTreeLeaf, page_id);
583 page.as_bytes_mut()[100] = 0xCD;
584 tx.write_page(page_id, page).unwrap();
585 tx.commit().unwrap();
586
587 db.sync().unwrap();
589
590 }
592
593 {
595 let db = Database::open(&path).unwrap();
596
597 let read_page = db.read_page(page_id).unwrap();
599 assert_eq!(read_page.as_bytes()[100], 0xCD);
600 }
601
602 cleanup(&path);
603 }
604
605 #[test]
606 fn test_database_checkpoint() {
607 let path = temp_db_path();
608 cleanup(&path);
609
610 {
611 let db = Database::open(&path).unwrap();
612
613 let page1 = db.allocate_page(PageType::BTreeLeaf).unwrap();
615 let page2 = db.allocate_page(PageType::BTreeLeaf).unwrap();
616
617 let mut tx1 = db.begin().unwrap();
619 let mut p1 = Page::new(PageType::BTreeLeaf, page1.page_id());
620 p1.as_bytes_mut()[100] = 0x11;
621 tx1.write_page(page1.page_id(), p1).unwrap();
622 tx1.commit().unwrap();
623
624 let mut tx2 = db.begin().unwrap();
625 let mut p2 = Page::new(PageType::BTreeLeaf, page2.page_id());
626 p2.as_bytes_mut()[100] = 0x22;
627 tx2.write_page(page2.page_id(), p2).unwrap();
628 tx2.commit().unwrap();
629
630 let result = db.checkpoint().unwrap();
632 assert_eq!(result.transactions_processed, 2);
633 assert!(result.pages_checkpointed >= 2);
634
635 db.close().unwrap();
637 }
638
639 {
641 let db = Database::open(&path).unwrap();
642 assert!(db.page_count() >= 3); }
645
646 cleanup(&path);
647 }
648
649 #[test]
650 fn test_database_read_only() {
651 let path = temp_db_path();
652 cleanup(&path);
653
654 {
656 let db = Database::open(&path).unwrap();
657 let page = db.allocate_page(PageType::BTreeLeaf).unwrap();
658 db.close().unwrap();
659 }
660
661 {
663 let config = DatabaseConfig {
664 read_only: true,
665 ..Default::default()
666 };
667 let db = Database::open_with_config(&path, config).unwrap();
668 assert!(db.is_read_only());
669
670 assert!(db.allocate_page(PageType::BTreeLeaf).is_err());
672 }
673
674 cleanup(&path);
675 }
676
677 #[test]
678 fn test_database_multiple_transactions() {
679 let path = temp_db_path();
680 cleanup(&path);
681
682 {
683 let db = Database::open(&path).unwrap();
684
685 let page1 = db.allocate_page(PageType::BTreeLeaf).unwrap();
687 let page2 = db.allocate_page(PageType::BTreeLeaf).unwrap();
688
689 let mut tx1 = db.begin().unwrap();
691 let mut tx2 = db.begin().unwrap();
692
693 let mut p1 = Page::new(PageType::BTreeLeaf, page1.page_id());
695 p1.as_bytes_mut()[100] = 0x11;
696 tx1.write_page(page1.page_id(), p1).unwrap();
697
698 let mut p2 = Page::new(PageType::BTreeLeaf, page2.page_id());
700 p2.as_bytes_mut()[100] = 0x22;
701 tx2.write_page(page2.page_id(), p2).unwrap();
702
703 tx1.commit().unwrap();
705 tx2.commit().unwrap();
706
707 let r1 = db.read_page(page1.page_id()).unwrap();
709 let r2 = db.read_page(page2.page_id()).unwrap();
710 assert_eq!(r1.as_bytes()[100], 0x11);
711 assert_eq!(r2.as_bytes()[100], 0x22);
712 }
713
714 cleanup(&path);
715 }
716
717 #[test]
718 fn test_begin_returns_structured_error_when_state_lock_is_poisoned() {
719 let path = temp_db_path();
720 cleanup(&path);
721
722 {
723 let db = Arc::new(Database::open(&path).unwrap());
724 let poison_target = Arc::clone(&db);
725 let _ = std::thread::spawn(move || {
726 let _guard = poison_target
727 .state
728 .write()
729 .expect("state lock should be acquired");
730 panic!("poison database state");
731 })
732 .join();
733
734 match db.begin() {
735 Ok(_) => panic!("begin should fail after state lock poisoning"),
736 Err(err) => {
737 assert!(matches!(err, DatabaseError::LockPoisoned("database state")))
738 }
739 }
740 }
741
742 cleanup(&path);
743 }
744}