1use std::{
23 fmt::Debug,
24 fs,
25 io::ErrorKind,
26 path::{Path, PathBuf},
27};
28
29use nautilus_core::UnixNanos;
30use redb::{
31 CommitError, Database, DatabaseError, Durability, ReadOnlyDatabase, ReadTransaction,
32 ReadableDatabase, ReadableTable, StorageError, TableDefinition, TableError, TransactionError,
33 WriteTransaction,
34};
35
36use crate::{
37 backend::{AppendEntry, EventStore, IndexKey, IndexKind, ScanDirection},
38 codec,
39 entry::EventStoreEntry,
40 error::EventStoreError,
41 format,
42 manifest::{RunManifest, RunStatus},
43 snapshot::{SnapshotAnchor, validate_new_anchor},
44};
45
46const ENTRIES_TABLE: TableDefinition<u64, &[u8]> = TableDefinition::new("entries");
47const MANIFEST_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("manifest");
48const CLIENT_ORDER_INDEX: TableDefinition<&str, u64> = TableDefinition::new("client_order_id_idx");
49const VENUE_ORDER_INDEX: TableDefinition<&str, u64> = TableDefinition::new("venue_order_id_idx");
50const SNAPSHOT_ANCHOR_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("snapshot_anchor");
51
52const MANIFEST_KEY: &str = "current";
53const SNAPSHOT_ANCHOR_KEY: &str = "latest";
54
55#[derive(Debug)]
64pub struct RedbBackend {
65 base_dir: PathBuf,
66 state: Option<RunState>,
67}
68
69#[derive(Debug)]
70struct RunState {
71 db: RunDatabase,
72 manifest: RunManifest,
73 high_watermark: u64,
74 max_ts_init: UnixNanos,
75 file_path: PathBuf,
76}
77
78enum RunDatabase {
79 ReadWrite(Database),
80 ReadOnly(ReadOnlyDatabase),
81}
82
83impl Debug for RunDatabase {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 match self {
86 Self::ReadWrite(_) => f.write_str("RunDatabase::ReadWrite"),
87 Self::ReadOnly(_) => f.write_str("RunDatabase::ReadOnly"),
88 }
89 }
90}
91
92impl RunDatabase {
93 fn readable(&self) -> &dyn ReadableDatabase {
94 match self {
95 Self::ReadWrite(db) => db,
96 Self::ReadOnly(db) => db,
97 }
98 }
99
100 fn read_write(&self) -> Result<&Database, EventStoreError> {
101 match self {
102 Self::ReadWrite(db) => Ok(db),
103 Self::ReadOnly(_) => Err(EventStoreError::Closed),
104 }
105 }
106
107 fn begin_read(&self) -> Result<ReadTransaction, EventStoreError> {
108 self.readable().begin_read().map_err(map_transaction_err)
109 }
110}
111
112impl RedbBackend {
113 #[must_use]
118 pub fn new(base_dir: impl Into<PathBuf>) -> Self {
119 Self {
120 base_dir: base_dir.into(),
121 state: None,
122 }
123 }
124
125 #[must_use]
127 pub fn run_dir(&self, instance_id: &str) -> PathBuf {
128 self.base_dir.join(instance_id)
129 }
130
131 #[must_use]
133 pub fn run_path(&self, instance_id: &str, run_id: &str) -> PathBuf {
134 self.run_dir(instance_id).join(format!("{run_id}.redb"))
135 }
136
137 pub fn current_path(&self) -> Result<&Path, EventStoreError> {
143 Ok(self.state()?.file_path.as_path())
144 }
145
146 pub fn open_sealed(
179 base_dir: impl Into<PathBuf>,
180 instance_id: &str,
181 run_id: &str,
182 ) -> Result<Self, EventStoreError> {
183 let base = base_dir.into();
184 let path = base.join(instance_id).join(format!("{run_id}.redb"));
185 Self::open_sealed_path(base, path)
186 }
187
188 pub fn open_sealed_file(path: impl Into<PathBuf>) -> Result<Self, EventStoreError> {
197 let path = path.into();
198 let base = path
199 .parent()
200 .and_then(Path::parent)
201 .map_or_else(PathBuf::new, Path::to_path_buf);
202 Self::open_sealed_path(base, path)
203 }
204
205 fn open_sealed_path(base: PathBuf, path: PathBuf) -> Result<Self, EventStoreError> {
206 if !path.exists() {
207 return Err(EventStoreError::Backend(format!(
208 "no run file at {}",
209 path.display()
210 )));
211 }
212
213 let db = ReadOnlyDatabase::open(&path).map_err(map_read_only_database_err)?;
214 format::verify_store_format(&db)?;
215 let manifest = Self::read_manifest(&db)?.ok_or_else(|| {
216 EventStoreError::Corrupted(format!(
217 "missing manifest in run file at {}",
218 path.display()
219 ))
220 })?;
221
222 if !manifest.is_sealed() {
223 return Err(EventStoreError::Backend(format!(
224 "run file at {} is not sealed, status was {:?}",
225 path.display(),
226 manifest.status,
227 )));
228 }
229 let (high_watermark, max_ts_init) = Self::compute_progress(&db)?;
230
231 Ok(Self {
232 base_dir: base,
233 state: Some(RunState {
234 db: RunDatabase::ReadOnly(db),
235 manifest,
236 high_watermark,
237 max_ts_init,
238 file_path: path,
239 }),
240 })
241 }
242
243 pub fn list_runs(
264 base_dir: &Path,
265 instance_id: &str,
266 ) -> Result<Vec<RunManifest>, EventStoreError> {
267 let dir = base_dir.join(instance_id);
268 let entries = match fs::read_dir(&dir) {
269 Ok(it) => it,
270 Err(e) if e.kind() == ErrorKind::NotFound => return Ok(Vec::new()),
271 Err(e) => {
272 return Err(EventStoreError::Backend(format!(
273 "read_dir {}: {e}",
274 dir.display()
275 )));
276 }
277 };
278
279 let mut manifests = Vec::new();
280
281 for entry in entries {
282 let entry = entry.map_err(|e| {
283 EventStoreError::Backend(format!("read_dir entry in {}: {e}", dir.display()))
284 })?;
285 let path = entry.path();
286
287 if !is_run_file(&path) {
288 continue;
289 }
290
291 match Self::read_run_manifest(&path) {
292 Ok(manifest) => manifests.push(manifest),
293 Err(e) if format::is_unsupported_store_format(&e) => return Err(e),
294 Err(e) => {
295 log::error!("Skipping unreadable run file {}: {e}", path.display());
296 }
297 }
298 }
299 manifests.sort_by(|a, b| {
303 a.start_ts_init
304 .cmp(&b.start_ts_init)
305 .then_with(|| a.run_id.cmp(&b.run_id))
306 });
307 Ok(manifests)
308 }
309
310 fn read_run_manifest(path: &Path) -> Result<RunManifest, EventStoreError> {
311 let manifest = match ReadOnlyDatabase::open(path) {
312 Ok(db) => {
313 Self::verify_listed_store_format(&db, path)?;
314 Self::read_manifest(&db)?
315 }
316 Err(DatabaseError::RepairAborted) => {
320 log::warn!(
321 "Run file {} was not shut down cleanly, repairing",
322 path.display()
323 );
324 let db = Database::open(path).map_err(map_database_err)?;
325 Self::verify_listed_store_format(&db, path)?;
326 Self::read_manifest(&db)?
327 }
328 Err(e) => return Err(map_read_only_database_err(e)),
329 };
330 manifest.ok_or_else(|| missing_manifest(path))
331 }
332
333 fn verify_listed_store_format<D: ReadableDatabase + ?Sized>(
334 db: &D,
335 path: &Path,
336 ) -> Result<(), EventStoreError> {
337 match format::verify_store_format(db) {
338 Ok(()) => Ok(()),
339 Err(e) if format::is_missing_store_format(&e) && !Self::manifest_row_exists(db)? => {
340 Err(missing_manifest(path))
341 }
342 Err(e) => Err(e),
343 }
344 }
345
346 fn manifest_row_exists<D: ReadableDatabase + ?Sized>(db: &D) -> Result<bool, EventStoreError> {
347 let txn = db.begin_read().map_err(map_transaction_err)?;
348 let table = match txn.open_table(MANIFEST_TABLE) {
349 Ok(table) => table,
350 Err(TableError::TableDoesNotExist(_)) => return Ok(false),
351 Err(e) => return Err(map_table_err(e)),
352 };
353
354 table
355 .get(MANIFEST_KEY)
356 .map(|value| value.is_some())
357 .map_err(map_storage_err)
358 }
359
360 fn state(&self) -> Result<&RunState, EventStoreError> {
361 self.state
362 .as_ref()
363 .ok_or_else(|| EventStoreError::Backend("no run open".to_string()))
364 }
365
366 fn state_mut(&mut self) -> Result<&mut RunState, EventStoreError> {
367 self.state
368 .as_mut()
369 .ok_or_else(|| EventStoreError::Backend("no run open".to_string()))
370 }
371
372 fn initialize_fresh(db: &Database, manifest: &RunManifest) -> Result<(), EventStoreError> {
373 let txn = begin_immediate_write(db)?;
374 {
375 txn.open_table(ENTRIES_TABLE).map_err(map_table_err)?;
376 txn.open_table(CLIENT_ORDER_INDEX).map_err(map_table_err)?;
377 txn.open_table(VENUE_ORDER_INDEX).map_err(map_table_err)?;
378 txn.open_table(SNAPSHOT_ANCHOR_TABLE)
379 .map_err(map_table_err)?;
380 }
381 format::write_store_format(&txn)?;
382 insert_run_manifest(&txn, manifest)?;
383 txn.commit().map_err(map_commit_err)?;
384 Ok(())
385 }
386
387 fn write_manifest(db: &Database, manifest: &RunManifest) -> Result<(), EventStoreError> {
388 let txn = begin_immediate_write(db)?;
389 insert_run_manifest(&txn, manifest)?;
390 txn.commit().map_err(map_commit_err)?;
391 Ok(())
392 }
393
394 fn read_manifest<D: ReadableDatabase + ?Sized>(
395 db: &D,
396 ) -> Result<Option<RunManifest>, EventStoreError> {
397 let txn = db.begin_read().map_err(map_transaction_err)?;
398 let table = txn.open_table(MANIFEST_TABLE).map_err(map_table_err)?;
399 let Some(value) = table.get(MANIFEST_KEY).map_err(map_storage_err)? else {
400 return Ok(None);
401 };
402 let bytes = value.value();
403 let manifest = codec::decode_from_slice::<RunManifest>(bytes)
404 .map_err(|e| EventStoreError::Corrupted(format!("decode manifest: {e}")))?;
405 Ok(Some(manifest))
406 }
407
408 fn read_snapshot_anchor<D: ReadableDatabase + ?Sized>(
409 db: &D,
410 ) -> Result<Option<SnapshotAnchor>, EventStoreError> {
411 let txn = db.begin_read().map_err(map_transaction_err)?;
412 let table = match txn.open_table(SNAPSHOT_ANCHOR_TABLE) {
413 Ok(table) => table,
414 Err(TableError::TableDoesNotExist(_)) => return Ok(None),
415 Err(e) => return Err(map_table_err(e)),
416 };
417 let Some(value) = table.get(SNAPSHOT_ANCHOR_KEY).map_err(map_storage_err)? else {
418 return Ok(None);
419 };
420 let bytes = value.value();
421 let anchor = codec::decode_from_slice::<SnapshotAnchor>(bytes)
422 .map_err(|e| EventStoreError::Corrupted(format!("decode snapshot anchor: {e}")))?;
423 Ok(Some(anchor))
424 }
425
426 fn compute_progress<D: ReadableDatabase + ?Sized>(
427 db: &D,
428 ) -> Result<(u64, UnixNanos), EventStoreError> {
429 let txn = db.begin_read().map_err(map_transaction_err)?;
430 let table = txn.open_table(ENTRIES_TABLE).map_err(map_table_err)?;
431
432 let Some((last_key, _)) = table.last().map_err(map_storage_err)? else {
433 return Ok((0, UnixNanos::default()));
434 };
435 let high_watermark = last_key.value();
436
437 let mut max_ts = UnixNanos::default();
443 let iter = table.iter().map_err(map_storage_err)?;
444
445 for row in iter {
446 let (key, value) = row.map_err(map_storage_err)?;
447 let bytes = value.value();
448
449 match codec::decode_from_slice::<EventStoreEntry>(bytes) {
450 Ok(entry) => {
451 if entry.ts_init > max_ts {
452 max_ts = entry.ts_init;
453 }
454 }
455 Err(e) => {
456 log::error!("Undecodable entry at seq {} on load: {e}", key.value());
457 }
458 }
459 }
460
461 Ok((high_watermark, max_ts))
462 }
463}
464
465impl EventStore for RedbBackend {
466 fn open_run(&mut self, mut manifest: RunManifest) -> Result<(), EventStoreError> {
467 if let Some(state) = &self.state {
468 if matches!(state.db, RunDatabase::ReadOnly(_)) {
469 return Err(EventStoreError::Closed);
470 }
471
472 if !state.manifest.is_sealed() {
473 return Err(EventStoreError::CrashedPredecessor);
474 }
475 }
476
477 let dir = self.run_dir(&manifest.instance_id);
478 fs::create_dir_all(&dir).map_err(|e| {
479 let msg = format!("create dir {}: {e}", dir.display());
480
481 if is_disk_pressure(e.kind()) {
482 EventStoreError::Disk(msg)
483 } else {
484 EventStoreError::Backend(msg)
485 }
486 })?;
487 let path = self.run_path(&manifest.instance_id, &manifest.run_id);
488 let path_existed = path.exists();
489
490 let db = Database::create(&path).map_err(map_database_err)?;
491
492 if path_existed {
493 format::verify_store_format(&db)?;
494 let on_disk = Self::read_manifest(&db)?.ok_or_else(|| {
495 EventStoreError::Corrupted(format!(
496 "missing manifest in existing run file at {}",
497 path.display()
498 ))
499 })?;
500
501 if !matches!(on_disk.status, RunStatus::Running) {
502 return Err(EventStoreError::Backend(format!(
503 "run file at {} already sealed, status was {:?}",
504 path.display(),
505 on_disk.status
506 )));
507 }
508
509 let (high_watermark, max_ts_init) = Self::compute_progress(&db)?;
510 let mut recovered = on_disk;
511 recovered.high_watermark = high_watermark;
512 self.state = Some(RunState {
513 db: RunDatabase::ReadWrite(db),
514 manifest: recovered,
515 high_watermark,
516 max_ts_init,
517 file_path: path,
518 });
519 return Err(EventStoreError::CrashedPredecessor);
520 }
521
522 manifest.status = RunStatus::Running;
523 manifest.end_ts_init = None;
524 manifest.high_watermark = 0;
525 Self::initialize_fresh(&db, &manifest)?;
526
527 self.state = Some(RunState {
528 db: RunDatabase::ReadWrite(db),
529 manifest,
530 high_watermark: 0,
531 max_ts_init: UnixNanos::default(),
532 file_path: path,
533 });
534 Ok(())
535 }
536
537 fn append_batch(&mut self, entries: &[AppendEntry]) -> Result<u64, EventStoreError> {
538 let state = self.state_mut()?;
539
540 if state.manifest.is_sealed() {
541 return Err(EventStoreError::Closed);
542 }
543
544 if entries.is_empty() {
545 return Ok(state.high_watermark);
546 }
547
548 for (expected, append) in (state.high_watermark + 1..).zip(entries.iter()) {
549 if append.entry.seq != expected {
550 return Err(EventStoreError::OutOfOrder {
554 high_watermark: state.high_watermark,
555 seq: append.entry.seq,
556 });
557 }
558 }
559
560 let encoded: Vec<Vec<u8>> = entries
561 .iter()
562 .map(|append| {
563 codec::encode_to_vec(&append.entry).map_err(|e| {
564 EventStoreError::Backend(format!("encode entry seq={}: {e}", append.entry.seq))
565 })
566 })
567 .collect::<Result<_, _>>()?;
568
569 let db = state.db.read_write()?;
570 let txn = begin_immediate_write(db)?;
571 {
572 let mut entries_table = txn.open_table(ENTRIES_TABLE).map_err(map_table_err)?;
573 let mut client_table = txn.open_table(CLIENT_ORDER_INDEX).map_err(map_table_err)?;
574 let mut venue_table = txn.open_table(VENUE_ORDER_INDEX).map_err(map_table_err)?;
575
576 for (append, bytes) in entries.iter().zip(encoded.iter()) {
577 entries_table
578 .insert(append.entry.seq, bytes.as_slice())
579 .map_err(map_storage_err)?;
580
581 for IndexKey { kind, key } in &append.index_keys {
582 let table = match kind {
583 IndexKind::ClientOrderId => &mut client_table,
584 IndexKind::VenueOrderId => &mut venue_table,
585 };
586 let already = table.get(key.as_str()).map_err(map_storage_err)?.is_some();
587
588 if !already {
589 table
590 .insert(key.as_str(), append.entry.seq)
591 .map_err(map_storage_err)?;
592 }
593 }
594 }
595 }
596 txn.commit().map_err(map_commit_err)?;
597
598 let mut max_ts = state.max_ts_init;
599 let mut new_hwm = state.high_watermark;
600
601 for append in entries {
602 if append.entry.ts_init > max_ts {
603 max_ts = append.entry.ts_init;
604 }
605 new_hwm = append.entry.seq;
606 }
607 state.high_watermark = new_hwm;
608 state.max_ts_init = max_ts;
609 state.manifest.high_watermark = new_hwm;
610
611 Ok(new_hwm)
612 }
613
614 fn scan_range(
615 &self,
616 from: u64,
617 to: u64,
618 direction: ScanDirection,
619 ) -> Result<Vec<EventStoreEntry>, EventStoreError> {
620 let state = self.state()?;
621
622 if from > to || from == 0 || state.high_watermark == 0 {
623 return Ok(Vec::new());
624 }
625
626 let lo = from;
627 let hi = to.min(state.high_watermark);
628
629 if lo > hi {
630 return Ok(Vec::new());
631 }
632
633 let txn = state.db.begin_read()?;
634 let table = txn.open_table(ENTRIES_TABLE).map_err(map_table_err)?;
635
636 let mut out = Vec::new();
641 let mut expected = lo;
642 let iter = table.range(lo..=hi).map_err(map_storage_err)?;
643
644 for row in iter {
645 let (k, v) = row.map_err(map_storage_err)?;
646 let seq = k.value();
647
648 if seq != expected {
649 return Err(EventStoreError::Gap {
650 prev: expected.saturating_sub(1),
651 next: seq,
652 missing: expected,
653 });
654 }
655 let bytes = v.value();
656 let entry = codec::decode_from_slice::<EventStoreEntry>(bytes)
657 .map_err(|e| EventStoreError::Corrupted(format!("decode entry seq={seq}: {e}")))?;
658
659 check_embedded_seq(seq, &entry)?;
660
661 if entry.recompute_hash() != entry.entry_hash {
662 return Err(EventStoreError::HashMismatch { seq });
663 }
664 out.push(entry);
665 expected = seq + 1;
666 }
667
668 if expected <= hi {
669 return Err(EventStoreError::Gap {
670 prev: expected.saturating_sub(1),
671 next: hi + 1,
672 missing: expected,
673 });
674 }
675
676 if matches!(direction, ScanDirection::Reverse) {
677 out.reverse();
678 }
679 Ok(out)
680 }
681
682 fn scan_seq(&self, seq: u64) -> Result<Option<EventStoreEntry>, EventStoreError> {
683 let state = self.state()?;
684
685 if seq == 0 || seq > state.high_watermark {
686 return Ok(None);
687 }
688
689 let txn = state.db.begin_read()?;
690 let table = txn.open_table(ENTRIES_TABLE).map_err(map_table_err)?;
691 let Some(value) = table.get(seq).map_err(map_storage_err)? else {
692 return Err(EventStoreError::Gap {
695 prev: seq.saturating_sub(1),
696 next: seq + 1,
697 missing: seq,
698 });
699 };
700
701 let bytes = value.value();
702 let entry = codec::decode_from_slice::<EventStoreEntry>(bytes)
703 .map_err(|e| EventStoreError::Corrupted(format!("decode entry seq={seq}: {e}")))?;
704
705 check_embedded_seq(seq, &entry)?;
706
707 if entry.recompute_hash() != entry.entry_hash {
708 return Err(EventStoreError::HashMismatch { seq });
709 }
710 Ok(Some(entry))
711 }
712
713 fn lookup(&self, kind: IndexKind, key: &str) -> Result<Option<u64>, EventStoreError> {
714 let state = self.state()?;
715 let txn = state.db.begin_read()?;
716 let definition = match kind {
717 IndexKind::ClientOrderId => CLIENT_ORDER_INDEX,
718 IndexKind::VenueOrderId => VENUE_ORDER_INDEX,
719 };
720 let table = txn.open_table(definition).map_err(map_table_err)?;
721 let value = table.get(key).map_err(map_storage_err)?;
722 Ok(value.map(|v| v.value()))
723 }
724
725 fn iter_index_keys(&self, kind: IndexKind) -> Result<Vec<(String, u64)>, EventStoreError> {
726 let state = self.state()?;
727 let txn = state.db.begin_read()?;
728 let definition = match kind {
729 IndexKind::ClientOrderId => CLIENT_ORDER_INDEX,
730 IndexKind::VenueOrderId => VENUE_ORDER_INDEX,
731 };
732 let table = txn.open_table(definition).map_err(map_table_err)?;
733 let iter = table.iter().map_err(map_storage_err)?;
734 let mut out = Vec::new();
735
736 for row in iter {
737 let (k, v) = row.map_err(map_storage_err)?;
738 out.push((k.value().to_string(), v.value()));
739 }
740 Ok(out)
741 }
742
743 fn record_snapshot_anchor(&mut self, anchor: SnapshotAnchor) -> Result<(), EventStoreError> {
744 let state = self.state_mut()?;
745
746 if state.manifest.is_sealed() {
747 return Err(EventStoreError::Closed);
748 }
749
750 let latest = Self::read_snapshot_anchor(state.db.readable())?;
751 validate_new_anchor(&anchor, state.high_watermark, latest.as_ref())?;
752
753 let bytes = codec::encode_to_vec(&anchor)
754 .map_err(|e| EventStoreError::Backend(format!("encode snapshot anchor: {e}")))?;
755 let db = state.db.read_write()?;
756 let txn = begin_immediate_write(db)?;
757 {
758 let mut table = txn
759 .open_table(SNAPSHOT_ANCHOR_TABLE)
760 .map_err(map_table_err)?;
761 table
762 .insert(SNAPSHOT_ANCHOR_KEY, bytes.as_slice())
763 .map_err(map_storage_err)?;
764 }
765 txn.commit().map_err(map_commit_err)?;
766 Ok(())
767 }
768
769 fn latest_snapshot_anchor(&self) -> Result<Option<SnapshotAnchor>, EventStoreError> {
770 Self::read_snapshot_anchor(self.state()?.db.readable())
771 }
772
773 fn seal(&mut self, status: RunStatus) -> Result<(), EventStoreError> {
774 let state = self.state_mut()?;
775
776 if matches!(status, RunStatus::Running) {
779 return Err(EventStoreError::Backend(
780 "seal status must be a terminal state, was Running".to_string(),
781 ));
782 }
783
784 if state.manifest.is_sealed() {
785 return Err(EventStoreError::Closed);
786 }
787
788 let mut updated = state.manifest.clone();
789 updated.status = status;
790 updated.high_watermark = state.high_watermark;
791
792 if state.high_watermark > 0 {
793 updated.end_ts_init = Some(state.max_ts_init);
794 }
795
796 Self::write_manifest(state.db.read_write()?, &updated)?;
797 state.manifest = updated;
798 Ok(())
799 }
800
801 fn manifest(&self) -> Result<RunManifest, EventStoreError> {
802 Ok(self.state()?.manifest.clone())
803 }
804
805 fn high_watermark(&self) -> Result<u64, EventStoreError> {
806 Ok(self.state()?.high_watermark)
807 }
808}
809
810fn missing_manifest(path: &Path) -> EventStoreError {
811 EventStoreError::Corrupted(format!(
812 "missing manifest in run file at {}",
813 path.display()
814 ))
815}
816
817fn check_embedded_seq(seq: u64, entry: &EventStoreEntry) -> Result<(), EventStoreError> {
820 if entry.seq != seq {
821 return Err(EventStoreError::SeqMismatch {
822 table_key: seq,
823 embedded_seq: entry.seq,
824 });
825 }
826 Ok(())
827}
828
829fn begin_immediate_write(db: &Database) -> Result<WriteTransaction, EventStoreError> {
830 let mut txn = db.begin_write().map_err(map_transaction_err)?;
831 txn.set_durability(Durability::Immediate)
832 .map_err(|e| EventStoreError::Backend(format!("set durability: {e}")))?;
833 Ok(txn)
834}
835
836fn insert_run_manifest(
837 txn: &WriteTransaction,
838 manifest: &RunManifest,
839) -> Result<(), EventStoreError> {
840 let bytes = codec::encode_to_vec(manifest)
841 .map_err(|e| EventStoreError::Backend(format!("encode manifest: {e}")))?;
842 let mut table = txn.open_table(MANIFEST_TABLE).map_err(map_table_err)?;
843 table
844 .insert(MANIFEST_KEY, bytes.as_slice())
845 .map_err(map_storage_err)?;
846 Ok(())
847}
848
849fn map_storage_err(err: StorageError) -> EventStoreError {
850 match err {
851 StorageError::Io(io_err) if is_disk_pressure(io_err.kind()) => {
852 EventStoreError::Disk(io_err.to_string())
853 }
854 StorageError::Corrupted(msg) => EventStoreError::Corrupted(msg),
855 other => EventStoreError::Backend(other.to_string()),
856 }
857}
858
859fn is_disk_pressure(kind: ErrorKind) -> bool {
864 matches!(
865 kind,
866 ErrorKind::FileTooLarge | ErrorKind::StorageFull | ErrorKind::QuotaExceeded
867 )
868}
869
870fn map_database_err(err: DatabaseError) -> EventStoreError {
871 match err {
872 DatabaseError::RepairAborted => EventStoreError::Corrupted(
873 "database requires repair and cannot be verified read-only".to_string(),
874 ),
875 DatabaseError::UpgradeRequired(version) => EventStoreError::Corrupted(format!(
876 "database file format version {version} requires manual upgrade",
877 )),
878 DatabaseError::Storage(storage) => map_storage_err(storage),
879 other => EventStoreError::Backend(other.to_string()),
880 }
881}
882
883fn map_read_only_database_err(err: DatabaseError) -> EventStoreError {
884 match err {
885 DatabaseError::Storage(StorageError::Io(io_err)) if is_corrupt_read(io_err.kind()) => {
886 EventStoreError::Corrupted(format!("read-only open failed: {io_err}"))
887 }
888 other => map_database_err(other),
889 }
890}
891
892fn is_corrupt_read(kind: ErrorKind) -> bool {
893 matches!(kind, ErrorKind::UnexpectedEof | ErrorKind::InvalidData)
894}
895
896fn map_table_err(err: TableError) -> EventStoreError {
897 match err {
902 TableError::Storage(storage) => map_storage_err(storage),
903 TableError::TableDoesNotExist(_)
904 | TableError::TableTypeMismatch { .. }
905 | TableError::TableIsMultimap(_)
906 | TableError::TableIsNotMultimap(_)
907 | TableError::TypeDefinitionChanged { .. } => EventStoreError::Corrupted(err.to_string()),
908 other => EventStoreError::Backend(other.to_string()),
909 }
910}
911
912fn map_commit_err(err: CommitError) -> EventStoreError {
913 match err {
914 CommitError::Storage(storage) => map_storage_err(storage),
915 other => EventStoreError::Backend(other.to_string()),
916 }
917}
918
919fn is_run_file(path: &Path) -> bool {
920 path.extension().and_then(|s| s.to_str()) == Some("redb")
921 && path
922 .file_name()
923 .and_then(|s| s.to_str())
924 .is_none_or(|name| !name.ends_with(".markers.redb"))
925}
926
927fn map_transaction_err(err: TransactionError) -> EventStoreError {
928 match err {
929 TransactionError::Storage(storage) => map_storage_err(storage),
930 other => EventStoreError::Backend(other.to_string()),
931 }
932}
933
934#[cfg(test)]
935mod tests {
936 use rstest::rstest;
937 use tempfile::TempDir;
938
939 use super::*;
940
941 fn raw_run_path(base: &Path, run_id: &str) -> PathBuf {
942 let dir = base.join("trader-001");
943 std::fs::create_dir_all(&dir).expect("mkdir");
944 dir.join(format!("{run_id}.redb"))
945 }
946
947 fn create_pre_codec_run_file(path: &Path) {
948 let entries: TableDefinition<u64, &[u8]> = TableDefinition::new("entries");
949 let manifest: TableDefinition<&str, &[u8]> = TableDefinition::new("manifest");
950 let db = Database::create(path).expect("create redb");
951 let txn = db.begin_write().expect("begin write");
952 {
953 txn.open_table(entries).expect("open entries");
954 let mut table = txn.open_table(manifest).expect("open manifest");
955 table
956 .insert("current", b"old-format".as_slice())
957 .expect("insert");
958 }
959 txn.commit().expect("commit");
960 }
961
962 #[rstest]
963 fn read_run_manifest_rejects_store_without_format_marker() {
964 let tmp = TempDir::new().expect("tempdir");
965 let path = raw_run_path(tmp.path(), "run-old-format");
966 create_pre_codec_run_file(&path);
967
968 let err = RedbBackend::read_run_manifest(&path).expect_err("must reject old format");
969
970 match err {
971 EventStoreError::Corrupted(msg) => {
972 assert!(msg.contains("regenerated"), "msg was: {msg}");
973 }
974 other => panic!("expected Corrupted, was {other:?}"),
975 }
976 }
977
978 #[rstest]
979 #[case::file_too_large(ErrorKind::FileTooLarge, true)]
980 #[case::storage_full(ErrorKind::StorageFull, true)]
981 #[case::quota_exceeded(ErrorKind::QuotaExceeded, true)]
982 #[case::other(ErrorKind::Other, false)]
983 #[case::not_found(ErrorKind::NotFound, false)]
984 #[case::permission_denied(ErrorKind::PermissionDenied, false)]
985 #[case::interrupted(ErrorKind::Interrupted, false)]
986 fn is_disk_pressure_matches_documented_kinds(#[case] kind: ErrorKind, #[case] expected: bool) {
987 assert_eq!(is_disk_pressure(kind), expected);
988 }
989
990 #[rstest]
991 fn map_storage_err_classifies_disk_pressure_as_disk() {
992 let io_err = std::io::Error::from(ErrorKind::StorageFull);
993 let mapped = map_storage_err(StorageError::Io(io_err));
994
995 match mapped {
996 EventStoreError::Disk(_) => {}
997 other => panic!("expected Disk, was {other:?}"),
998 }
999 }
1000
1001 #[rstest]
1002 fn map_storage_err_classifies_corrupted_as_corrupted() {
1003 let mapped = map_storage_err(StorageError::Corrupted("boom".to_string()));
1004
1005 match mapped {
1006 EventStoreError::Corrupted(msg) => assert!(msg.contains("boom")),
1007 other => panic!("expected Corrupted, was {other:?}"),
1008 }
1009 }
1010
1011 #[rstest]
1012 fn map_storage_err_falls_back_to_backend_for_unrelated_io() {
1013 let io_err = std::io::Error::from(ErrorKind::PermissionDenied);
1014 let mapped = map_storage_err(StorageError::Io(io_err));
1015
1016 match mapped {
1017 EventStoreError::Backend(_) => {}
1018 other => panic!("expected Backend, was {other:?}"),
1019 }
1020 }
1021}