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(), 3); }
521
522 {
524 let db = Database::open(&path).unwrap();
525 assert_eq!(db.page_count(), 3);
526 }
527
528 cleanup(&path);
529 }
530
531 #[test]
532 fn test_database_transaction() {
533 let path = temp_db_path();
534 cleanup(&path);
535
536 {
537 let db = Database::open(&path).unwrap();
538
539 let page = db.allocate_page(PageType::BTreeLeaf).unwrap();
541 let page_id = page.page_id();
542
543 let mut tx = db.begin().unwrap();
545
546 let mut page = Page::new(PageType::BTreeLeaf, page_id);
548 page.as_bytes_mut()[100] = 0xAB;
549 tx.write_page(page_id, page).unwrap();
550
551 tx.commit().unwrap();
553
554 let read_page = db.read_page(page_id).unwrap();
556 assert_eq!(read_page.as_bytes()[100], 0xAB);
557 }
558
559 cleanup(&path);
560 }
561
562 #[test]
563 fn test_database_crash_recovery() {
564 let path = temp_db_path();
565 cleanup(&path);
566
567 let page_id;
568
569 {
571 let db = Database::open(&path).unwrap();
572
573 let page = db.allocate_page(PageType::BTreeLeaf).unwrap();
575 page_id = page.page_id();
576
577 let mut tx = db.begin().unwrap();
579 let mut page = Page::new(PageType::BTreeLeaf, page_id);
580 page.as_bytes_mut()[100] = 0xCD;
581 tx.write_page(page_id, page).unwrap();
582 tx.commit().unwrap();
583
584 db.sync().unwrap();
586
587 }
589
590 {
592 let db = Database::open(&path).unwrap();
593
594 let read_page = db.read_page(page_id).unwrap();
596 assert_eq!(read_page.as_bytes()[100], 0xCD);
597 }
598
599 cleanup(&path);
600 }
601
602 #[test]
603 fn test_database_checkpoint() {
604 let path = temp_db_path();
605 cleanup(&path);
606
607 {
608 let db = Database::open(&path).unwrap();
609
610 let page1 = db.allocate_page(PageType::BTreeLeaf).unwrap();
612 let page2 = db.allocate_page(PageType::BTreeLeaf).unwrap();
613
614 let mut tx1 = db.begin().unwrap();
616 let mut p1 = Page::new(PageType::BTreeLeaf, page1.page_id());
617 p1.as_bytes_mut()[100] = 0x11;
618 tx1.write_page(page1.page_id(), p1).unwrap();
619 tx1.commit().unwrap();
620
621 let mut tx2 = db.begin().unwrap();
622 let mut p2 = Page::new(PageType::BTreeLeaf, page2.page_id());
623 p2.as_bytes_mut()[100] = 0x22;
624 tx2.write_page(page2.page_id(), p2).unwrap();
625 tx2.commit().unwrap();
626
627 let result = db.checkpoint().unwrap();
629 assert_eq!(result.transactions_processed, 2);
630 assert!(result.pages_checkpointed >= 2);
631
632 db.close().unwrap();
634 }
635
636 {
638 let db = Database::open(&path).unwrap();
639 assert!(db.page_count() >= 3); }
642
643 cleanup(&path);
644 }
645
646 #[test]
647 fn test_database_read_only() {
648 let path = temp_db_path();
649 cleanup(&path);
650
651 {
653 let db = Database::open(&path).unwrap();
654 let page = db.allocate_page(PageType::BTreeLeaf).unwrap();
655 db.close().unwrap();
656 }
657
658 {
660 let config = DatabaseConfig {
661 read_only: true,
662 ..Default::default()
663 };
664 let db = Database::open_with_config(&path, config).unwrap();
665 assert!(db.is_read_only());
666
667 assert!(db.allocate_page(PageType::BTreeLeaf).is_err());
669 }
670
671 cleanup(&path);
672 }
673
674 #[test]
675 fn test_database_multiple_transactions() {
676 let path = temp_db_path();
677 cleanup(&path);
678
679 {
680 let db = Database::open(&path).unwrap();
681
682 let page1 = db.allocate_page(PageType::BTreeLeaf).unwrap();
684 let page2 = db.allocate_page(PageType::BTreeLeaf).unwrap();
685
686 let mut tx1 = db.begin().unwrap();
688 let mut tx2 = db.begin().unwrap();
689
690 let mut p1 = Page::new(PageType::BTreeLeaf, page1.page_id());
692 p1.as_bytes_mut()[100] = 0x11;
693 tx1.write_page(page1.page_id(), p1).unwrap();
694
695 let mut p2 = Page::new(PageType::BTreeLeaf, page2.page_id());
697 p2.as_bytes_mut()[100] = 0x22;
698 tx2.write_page(page2.page_id(), p2).unwrap();
699
700 tx1.commit().unwrap();
702 tx2.commit().unwrap();
703
704 let r1 = db.read_page(page1.page_id()).unwrap();
706 let r2 = db.read_page(page2.page_id()).unwrap();
707 assert_eq!(r1.as_bytes()[100], 0x11);
708 assert_eq!(r2.as_bytes()[100], 0x22);
709 }
710
711 cleanup(&path);
712 }
713
714 #[test]
715 fn test_begin_returns_structured_error_when_state_lock_is_poisoned() {
716 let path = temp_db_path();
717 cleanup(&path);
718
719 {
720 let db = Arc::new(Database::open(&path).unwrap());
721 let poison_target = Arc::clone(&db);
722 let _ = std::thread::spawn(move || {
723 let _guard = poison_target
724 .state
725 .write()
726 .expect("state lock should be acquired");
727 panic!("poison database state");
728 })
729 .join();
730
731 match db.begin() {
732 Ok(_) => panic!("begin should fail after state lock poisoning"),
733 Err(err) => {
734 assert!(matches!(err, DatabaseError::LockPoisoned("database state")))
735 }
736 }
737 }
738
739 cleanup(&path);
740 }
741}