1use std::{
19 fmt::Debug,
20 fs,
21 io::ErrorKind,
22 path::{Path, PathBuf},
23};
24
25use redb::{
26 CommitError, Database, DatabaseError, Durability, ReadOnlyDatabase, ReadTransaction,
27 ReadableDatabase, ReadableTable, StorageError, TableDefinition, TableError, TransactionError,
28 WriteTransaction,
29};
30use serde::{Serialize, de::DeserializeOwned};
31
32use crate::{
33 codec,
34 error::EventStoreError,
35 format,
36 manifest::RunStatus,
37 markers::{
38 DataCursorSnapshot, HiFiMarker, MarkerBackend, MarkerGap, MarkerManifest,
39 StoredMarkerRecord, StreamDictEntry,
40 },
41};
42
43const CURSOR_SNAPSHOTS_TABLE: TableDefinition<u64, &[u8]> =
44 TableDefinition::new("cursor_snapshots");
45const HIFI_MARKERS_TABLE: TableDefinition<u64, &[u8]> = TableDefinition::new("hifi_markers");
46const MARKER_GAPS_TABLE: TableDefinition<u64, &[u8]> = TableDefinition::new("marker_gaps");
47const STREAM_DICT_TABLE: TableDefinition<u32, &[u8]> = TableDefinition::new("stream_dict");
48const MARKER_MANIFEST_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("marker_manifest");
49
50const MANIFEST_KEY: &str = "current";
51
52#[derive(Debug)]
61pub struct RedbMarkerBackend {
62 file_path: PathBuf,
63 state: Option<RunState>,
64}
65
66#[derive(Debug)]
67struct RunState {
68 db: MarkerDatabase,
69 manifest: MarkerManifest,
70 file_path: PathBuf,
71}
72
73enum MarkerDatabase {
74 ReadWrite(Database),
75 ReadOnly(ReadOnlyDatabase),
76}
77
78impl Debug for MarkerDatabase {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 match self {
81 Self::ReadWrite(_) => f.write_str("MarkerDatabase::ReadWrite"),
82 Self::ReadOnly(_) => f.write_str("MarkerDatabase::ReadOnly"),
83 }
84 }
85}
86
87impl MarkerDatabase {
88 fn readable(&self) -> &dyn ReadableDatabase {
89 match self {
90 Self::ReadWrite(db) => db,
91 Self::ReadOnly(db) => db,
92 }
93 }
94
95 fn read_write(&self) -> Result<&Database, EventStoreError> {
96 match self {
97 Self::ReadWrite(db) => Ok(db),
98 Self::ReadOnly(_) => Err(EventStoreError::Closed),
99 }
100 }
101
102 fn begin_read(&self) -> Result<ReadTransaction, EventStoreError> {
103 self.readable().begin_read().map_err(map_transaction_err)
104 }
105}
106
107impl RedbMarkerBackend {
108 #[must_use]
110 pub fn new(file_path: impl Into<PathBuf>) -> Self {
111 Self {
112 file_path: file_path.into(),
113 state: None,
114 }
115 }
116
117 pub fn current_path(&self) -> Result<&Path, EventStoreError> {
123 Ok(self.state()?.file_path.as_path())
124 }
125
126 pub fn open_read_only_file(path: impl Into<PathBuf>) -> Result<Self, EventStoreError> {
136 let path = path.into();
137 if !path.exists() {
138 return Err(EventStoreError::Backend(format!(
139 "no marker run file at {}",
140 path.display()
141 )));
142 }
143
144 let db = ReadOnlyDatabase::open(&path).map_err(map_database_err)?;
145 format::verify_store_format(&db)?;
146 let manifest = Self::read_manifest(&db)?.ok_or_else(|| {
147 EventStoreError::Corrupted(format!(
148 "missing marker manifest in run file at {}",
149 path.display()
150 ))
151 })?;
152
153 Ok(Self {
154 file_path: path.clone(),
155 state: Some(RunState {
156 db: MarkerDatabase::ReadOnly(db),
157 manifest,
158 file_path: path,
159 }),
160 })
161 }
162
163 fn state(&self) -> Result<&RunState, EventStoreError> {
164 self.state
165 .as_ref()
166 .ok_or_else(|| EventStoreError::Backend("no marker run open".to_string()))
167 }
168
169 fn state_mut(&mut self) -> Result<&mut RunState, EventStoreError> {
170 self.state
171 .as_mut()
172 .ok_or_else(|| EventStoreError::Backend("no marker run open".to_string()))
173 }
174
175 fn writable_state(&mut self) -> Result<&mut RunState, EventStoreError> {
176 let state = self.state_mut()?;
177 if state.manifest.is_sealed() {
178 return Err(EventStoreError::Closed);
179 }
180 state.db.read_write()?;
181 Ok(state)
182 }
183
184 fn initialize_fresh(db: &Database, manifest: &MarkerManifest) -> Result<(), EventStoreError> {
185 let txn = begin_immediate_write(db)?;
186 {
187 txn.open_table(CURSOR_SNAPSHOTS_TABLE)
188 .map_err(map_table_err)?;
189 txn.open_table(HIFI_MARKERS_TABLE).map_err(map_table_err)?;
190 txn.open_table(MARKER_GAPS_TABLE).map_err(map_table_err)?;
191 txn.open_table(STREAM_DICT_TABLE).map_err(map_table_err)?;
192 }
193 format::write_store_format(&txn)?;
194 insert_marker_manifest(&txn, manifest)?;
195 txn.commit().map_err(map_commit_err)?;
196 Ok(())
197 }
198
199 fn write_manifest(db: &Database, manifest: &MarkerManifest) -> Result<(), EventStoreError> {
200 let txn = begin_immediate_write(db)?;
201 insert_marker_manifest(&txn, manifest)?;
202 txn.commit().map_err(map_commit_err)?;
203 Ok(())
204 }
205
206 fn read_manifest<D: ReadableDatabase + ?Sized>(
207 db: &D,
208 ) -> Result<Option<MarkerManifest>, EventStoreError> {
209 let txn = db.begin_read().map_err(map_transaction_err)?;
210 let table = txn
211 .open_table(MARKER_MANIFEST_TABLE)
212 .map_err(map_table_err)?;
213 let Some(value) = table.get(MANIFEST_KEY).map_err(map_storage_err)? else {
214 return Ok(None);
215 };
216 let bytes = value.value();
217 Ok(Some(decode_value("marker manifest", bytes)?))
218 }
219}
220
221impl MarkerBackend for RedbMarkerBackend {
222 fn open_run(&mut self, mut manifest: MarkerManifest) -> Result<(), EventStoreError> {
223 self.state = None;
224
225 if let Some(parent) = self
226 .file_path
227 .parent()
228 .filter(|parent| !parent.as_os_str().is_empty())
229 {
230 fs::create_dir_all(parent).map_err(|e| {
231 let msg = format!("create dir {}: {e}", parent.display());
232
233 if is_disk_pressure(e.kind()) {
234 EventStoreError::Disk(msg)
235 } else {
236 EventStoreError::Backend(msg)
237 }
238 })?;
239 }
240
241 let path = self.file_path.clone();
242 let path_existed = path.exists();
243 let db = Database::create(&path).map_err(map_database_err)?;
244
245 if path_existed {
246 format::verify_store_format(&db)?;
247 let on_disk = Self::read_manifest(&db)?.ok_or_else(|| {
248 EventStoreError::Corrupted(format!(
249 "missing marker manifest in existing run file at {}",
250 path.display()
251 ))
252 })?;
253
254 if on_disk.run_id != manifest.run_id {
255 return Err(EventStoreError::Backend(format!(
256 "marker run file at {} belongs to run_id {}, not {}",
257 path.display(),
258 on_disk.run_id,
259 manifest.run_id
260 )));
261 }
262
263 let mut opened = on_disk;
264 if matches!(opened.status, RunStatus::Running) {
265 opened.status = RunStatus::CrashedRecovered;
266 Self::write_manifest(&db, &opened)?;
267 }
268
269 self.state = Some(RunState {
270 db: MarkerDatabase::ReadWrite(db),
271 manifest: opened,
272 file_path: path,
273 });
274 return Ok(());
275 }
276
277 manifest.status = RunStatus::Running;
278 manifest.snapshot_count = 0;
279 manifest.hifi_count = 0;
280 manifest.gap_count = 0;
281 manifest.dict_count = 0;
282 Self::initialize_fresh(&db, &manifest)?;
283
284 self.state = Some(RunState {
285 db: MarkerDatabase::ReadWrite(db),
286 manifest,
287 file_path: path,
288 });
289 Ok(())
290 }
291
292 fn append_snapshot(
293 &mut self,
294 snapshot: &DataCursorSnapshot,
295 hash: [u8; 32],
296 ) -> Result<(), EventStoreError> {
297 let state = self.writable_state()?;
298 let stored = StoredMarkerRecord {
299 record: snapshot.clone(),
300 hash,
301 };
302 let record_bytes = encode_value("cursor snapshot", &stored)?;
303 let mut updated = state.manifest.clone();
304 updated.snapshot_count += 1;
305 let db = state.db.read_write()?;
306 append_u64_marker_record(
307 db,
308 CURSOR_SNAPSHOTS_TABLE,
309 snapshot.marker_seq,
310 record_bytes.as_slice(),
311 &updated,
312 )?;
313
314 state.manifest = updated;
315 Ok(())
316 }
317
318 fn append_hifi(&mut self, marker: &HiFiMarker, hash: [u8; 32]) -> Result<(), EventStoreError> {
319 let state = self.writable_state()?;
320 let stored = StoredMarkerRecord {
321 record: marker.clone(),
322 hash,
323 };
324 let record_bytes = encode_value("hifi marker", &stored)?;
325 let mut updated = state.manifest.clone();
326 updated.hifi_count += 1;
327 let db = state.db.read_write()?;
328 append_u64_marker_record(
329 db,
330 HIFI_MARKERS_TABLE,
331 marker.marker_seq,
332 record_bytes.as_slice(),
333 &updated,
334 )?;
335
336 state.manifest = updated;
337 Ok(())
338 }
339
340 fn append_gap(&mut self, gap: &MarkerGap, hash: [u8; 32]) -> Result<(), EventStoreError> {
341 let state = self.writable_state()?;
342 let stored = StoredMarkerRecord {
343 record: gap.clone(),
344 hash,
345 };
346 let record_bytes = encode_value("marker gap", &stored)?;
347 let mut updated = state.manifest.clone();
348 updated.gap_count += 1;
349 let db = state.db.read_write()?;
350 append_u64_marker_record(
351 db,
352 MARKER_GAPS_TABLE,
353 gap.from_marker_seq,
354 record_bytes.as_slice(),
355 &updated,
356 )?;
357
358 state.manifest = updated;
359 Ok(())
360 }
361
362 fn put_dict(&mut self, entry: &StreamDictEntry, hash: [u8; 32]) -> Result<(), EventStoreError> {
363 let state = self.writable_state()?;
364 let stored = StoredMarkerRecord {
365 record: entry.clone(),
366 hash,
367 };
368 let record_bytes = encode_value("stream dict entry", &stored)?;
369 let mut updated = state.manifest.clone();
370 updated.dict_count += 1;
371 let mut inserted = false;
372
373 let db = state.db.read_write()?;
374 let txn = begin_immediate_write(db)?;
375 {
376 let mut table = txn.open_table(STREAM_DICT_TABLE).map_err(map_table_err)?;
377 let already = table.get(entry.slot).map_err(map_storage_err)?.is_some();
378
379 if !already {
380 table
381 .insert(entry.slot, record_bytes.as_slice())
382 .map_err(map_storage_err)?;
383 inserted = true;
384 }
385 }
386
387 if inserted {
388 insert_marker_manifest(&txn, &updated)?;
389 }
390
391 txn.commit().map_err(map_commit_err)?;
392
393 if inserted {
394 state.manifest = updated;
395 }
396 Ok(())
397 }
398
399 fn scan_snapshots(&self) -> Result<Vec<DataCursorSnapshot>, EventStoreError> {
400 let out = self
401 .scan_snapshot_records()?
402 .unwrap_or_default()
403 .into_iter()
404 .map(|stored| stored.record)
405 .collect();
406 Ok(out)
407 }
408
409 fn scan_snapshot_records(
410 &self,
411 ) -> Result<Option<Vec<StoredMarkerRecord<DataCursorSnapshot>>>, EventStoreError> {
412 let state = self.state()?;
413 let txn = state.db.begin_read()?;
414 let table = txn
415 .open_table(CURSOR_SNAPSHOTS_TABLE)
416 .map_err(map_table_err)?;
417 let iter = table.iter().map_err(map_storage_err)?;
418 let mut out = Vec::new();
419
420 for row in iter {
421 let (_, value) = row.map_err(map_storage_err)?;
422 let stored: StoredMarkerRecord<DataCursorSnapshot> =
423 decode_value("cursor snapshot", value.value())?;
424 out.push(stored);
425 }
426 Ok(Some(out))
427 }
428
429 fn scan_hifi(&self) -> Result<Vec<HiFiMarker>, EventStoreError> {
430 let out = self
431 .scan_hifi_records()?
432 .unwrap_or_default()
433 .into_iter()
434 .map(|stored| stored.record)
435 .collect();
436 Ok(out)
437 }
438
439 fn scan_hifi_records(
440 &self,
441 ) -> Result<Option<Vec<StoredMarkerRecord<HiFiMarker>>>, EventStoreError> {
442 let state = self.state()?;
443 let txn = state.db.begin_read()?;
444 let table = txn.open_table(HIFI_MARKERS_TABLE).map_err(map_table_err)?;
445 let iter = table.iter().map_err(map_storage_err)?;
446 let mut out = Vec::new();
447
448 for row in iter {
449 let (_, value) = row.map_err(map_storage_err)?;
450 let stored: StoredMarkerRecord<HiFiMarker> =
451 decode_value("hifi marker", value.value())?;
452 out.push(stored);
453 }
454 Ok(Some(out))
455 }
456
457 fn scan_gaps(&self) -> Result<Vec<MarkerGap>, EventStoreError> {
458 let out = self
459 .scan_gap_records()?
460 .unwrap_or_default()
461 .into_iter()
462 .map(|stored| stored.record)
463 .collect();
464 Ok(out)
465 }
466
467 fn scan_gap_records(
468 &self,
469 ) -> Result<Option<Vec<StoredMarkerRecord<MarkerGap>>>, EventStoreError> {
470 let state = self.state()?;
471 let txn = state.db.begin_read()?;
472 let table = txn.open_table(MARKER_GAPS_TABLE).map_err(map_table_err)?;
473 let iter = table.iter().map_err(map_storage_err)?;
474 let mut out = Vec::new();
475
476 for row in iter {
477 let (_, value) = row.map_err(map_storage_err)?;
478 let stored: StoredMarkerRecord<MarkerGap> = decode_value("marker gap", value.value())?;
479 out.push(stored);
480 }
481 Ok(Some(out))
482 }
483
484 fn scan_dict(&self) -> Result<Vec<StreamDictEntry>, EventStoreError> {
485 let out = self
486 .scan_dict_records()?
487 .unwrap_or_default()
488 .into_iter()
489 .map(|stored| stored.record)
490 .collect();
491 Ok(out)
492 }
493
494 fn scan_dict_records(
495 &self,
496 ) -> Result<Option<Vec<StoredMarkerRecord<StreamDictEntry>>>, EventStoreError> {
497 let state = self.state()?;
498 let txn = state.db.begin_read()?;
499 let table = txn.open_table(STREAM_DICT_TABLE).map_err(map_table_err)?;
500 let iter = table.iter().map_err(map_storage_err)?;
501 let mut out = Vec::new();
502
503 for row in iter {
504 let (_, value) = row.map_err(map_storage_err)?;
505 let stored: StoredMarkerRecord<StreamDictEntry> =
506 decode_value("stream dict entry", value.value())?;
507 out.push(stored);
508 }
509 Ok(Some(out))
510 }
511
512 fn seal(&mut self, status: RunStatus) -> Result<(), EventStoreError> {
513 let state = self.state_mut()?;
514
515 if matches!(status, RunStatus::Running) {
516 return Err(EventStoreError::Backend(
517 "seal status must be a terminal state, was Running".to_string(),
518 ));
519 }
520
521 if state.manifest.is_sealed() {
522 return Err(EventStoreError::Closed);
523 }
524
525 let mut updated = state.manifest.clone();
526 updated.status = status;
527 let db = state.db.read_write()?;
528 Self::write_manifest(db, &updated)?;
529 state.manifest = updated;
530 Ok(())
531 }
532
533 fn manifest(&self) -> Result<MarkerManifest, EventStoreError> {
534 Ok(self.state()?.manifest.clone())
535 }
536}
537
538fn append_u64_marker_record(
539 db: &Database,
540 table_def: TableDefinition<u64, &[u8]>,
541 key: u64,
542 record_bytes: &[u8],
543 manifest: &MarkerManifest,
544) -> Result<(), EventStoreError> {
545 let txn = begin_immediate_write(db)?;
546 {
547 let mut table = txn.open_table(table_def).map_err(map_table_err)?;
548 table.insert(key, record_bytes).map_err(map_storage_err)?;
549 }
550 insert_marker_manifest(&txn, manifest)?;
551 txn.commit().map_err(map_commit_err)?;
552 Ok(())
553}
554
555fn begin_immediate_write(db: &Database) -> Result<WriteTransaction, EventStoreError> {
556 let mut txn = db.begin_write().map_err(map_transaction_err)?;
557 txn.set_durability(Durability::Immediate)
558 .map_err(|e| EventStoreError::Backend(format!("set durability: {e}")))?;
559 Ok(txn)
560}
561
562fn insert_marker_manifest(
563 txn: &WriteTransaction,
564 manifest: &MarkerManifest,
565) -> Result<(), EventStoreError> {
566 let manifest_bytes = encode_value("marker manifest", manifest)?;
567 let mut table = txn
568 .open_table(MARKER_MANIFEST_TABLE)
569 .map_err(map_table_err)?;
570 table
571 .insert(MANIFEST_KEY, manifest_bytes.as_slice())
572 .map_err(map_storage_err)?;
573 Ok(())
574}
575
576fn encode_value<T: Serialize>(label: &str, value: &T) -> Result<Vec<u8>, EventStoreError> {
577 codec::encode_to_vec(value)
578 .map_err(|e| EventStoreError::Backend(format!("encode {label}: {e}")))
579}
580
581fn decode_value<T: DeserializeOwned>(label: &str, bytes: &[u8]) -> Result<T, EventStoreError> {
582 codec::decode_from_slice::<T>(bytes)
583 .map_err(|e| EventStoreError::Corrupted(format!("decode {label}: {e}")))
584}
585
586fn map_storage_err(err: StorageError) -> EventStoreError {
587 match err {
588 StorageError::Io(io_err) if is_disk_pressure(io_err.kind()) => {
589 EventStoreError::Disk(io_err.to_string())
590 }
591 StorageError::Corrupted(msg) => EventStoreError::Corrupted(msg),
592 other => EventStoreError::Backend(other.to_string()),
593 }
594}
595
596fn is_disk_pressure(kind: ErrorKind) -> bool {
597 matches!(
598 kind,
599 ErrorKind::FileTooLarge | ErrorKind::StorageFull | ErrorKind::QuotaExceeded
600 )
601}
602
603fn map_database_err(err: DatabaseError) -> EventStoreError {
604 match err {
605 DatabaseError::RepairAborted => EventStoreError::Corrupted(
606 "database requires repair and cannot be verified read-only".to_string(),
607 ),
608 DatabaseError::UpgradeRequired(version) => EventStoreError::Corrupted(format!(
609 "database file format version {version} requires manual upgrade",
610 )),
611 DatabaseError::Storage(storage) => map_storage_err(storage),
612 other => EventStoreError::Backend(other.to_string()),
613 }
614}
615
616fn map_table_err(err: TableError) -> EventStoreError {
617 match err {
618 TableError::Storage(storage) => map_storage_err(storage),
619 TableError::TableDoesNotExist(_)
620 | TableError::TableTypeMismatch { .. }
621 | TableError::TableIsMultimap(_)
622 | TableError::TableIsNotMultimap(_)
623 | TableError::TypeDefinitionChanged { .. } => EventStoreError::Corrupted(err.to_string()),
624 other => EventStoreError::Backend(other.to_string()),
625 }
626}
627
628fn map_commit_err(err: CommitError) -> EventStoreError {
629 match err {
630 CommitError::Storage(storage) => map_storage_err(storage),
631 other => EventStoreError::Backend(other.to_string()),
632 }
633}
634
635fn map_transaction_err(err: TransactionError) -> EventStoreError {
636 match err {
637 TransactionError::Storage(storage) => map_storage_err(storage),
638 other => EventStoreError::Backend(other.to_string()),
639 }
640}
641
642#[cfg(test)]
643mod tests {
644 use std::{
645 fmt::Debug,
646 path::{Path, PathBuf},
647 };
648
649 use nautilus_core::UnixNanos;
650 use rstest::{fixture, rstest};
651 use tempfile::TempDir;
652
653 use super::RedbMarkerBackend;
654 use crate::{
655 error::EventStoreError,
656 format,
657 manifest::RunStatus,
658 markers::{
659 DataClass, DataCursorSnapshot, HiFiMarker, MarkerBackend, MarkerGap, MarkerGapReason,
660 MarkerManifest, MarkerVerifier, StreamCursor, StreamDictEntry, compute_dict_hash,
661 compute_gap_hash, compute_hifi_hash, compute_marker_hash,
662 },
663 };
664
665 fn marker_path(base: &Path, instance_id: &str, run_id: &str) -> PathBuf {
666 base.join(instance_id)
667 .join(format!("{run_id}.markers.redb"))
668 }
669
670 fn manifest(run_id: &str) -> MarkerManifest {
671 MarkerManifest {
672 run_id: run_id.to_string(),
673 enabled_classes: vec![DataClass::Quote, DataClass::Trade],
674 high_fidelity: true,
675 snapshot_count: 0,
676 hifi_count: 0,
677 gap_count: 0,
678 dict_count: 0,
679 status: RunStatus::Running,
680 }
681 }
682
683 fn snapshot(marker_seq: u64, event_seq_before: u64) -> DataCursorSnapshot {
684 DataCursorSnapshot {
685 marker_seq,
686 event_seq_before,
687 ts_init: UnixNanos::from(1_700_000_000_000_000_000 + marker_seq),
688 advanced: vec![StreamCursor {
689 slot: 0,
690 ts_init_hi: UnixNanos::from(1_700_000_000_000_000_000 + marker_seq),
691 count: marker_seq,
692 }],
693 }
694 }
695
696 fn hifi(marker_seq: u64) -> HiFiMarker {
697 HiFiMarker {
698 marker_seq,
699 event_seq_before: 42,
700 slot: 0,
701 ts_event: UnixNanos::from(1_700_000_000_000_000_100 + marker_seq),
702 ts_init: UnixNanos::from(1_700_000_000_000_000_200 + marker_seq),
703 same_ts_ordinal: 0,
704 record_fingerprint: [7u8; 32],
705 }
706 }
707
708 fn dict(slot: u32, data_cls: DataClass, identifier: &str) -> StreamDictEntry {
709 StreamDictEntry {
710 slot,
711 data_cls,
712 identifier: identifier.to_string(),
713 }
714 }
715
716 fn gap(from_marker_seq: u64, to_marker_seq: u64) -> MarkerGap {
717 MarkerGap {
718 from_marker_seq,
719 to_marker_seq,
720 reason: MarkerGapReason::Overflow,
721 }
722 }
723
724 fn assert_closed<T: Debug>(result: Result<T, EventStoreError>) {
725 match result {
726 Err(EventStoreError::Closed) => {}
727 other => panic!("expected Closed error, was {other:?}"),
728 }
729 }
730
731 fn assert_backend_message<T: Debug>(result: Result<T, EventStoreError>, expected: &str) {
732 match result {
733 Err(EventStoreError::Backend(message)) => assert_eq!(message, expected),
734 other => panic!("expected Backend error, was {other:?}"),
735 }
736 }
737
738 #[fixture]
739 fn temp_dir() -> TempDir {
740 TempDir::new().expect("create temp dir")
741 }
742
743 fn create_pre_codec_marker_file(path: &Path) {
744 let manifest: redb::TableDefinition<&str, &[u8]> =
745 redb::TableDefinition::new("marker_manifest");
746 let db = redb::Database::create(path).expect("create redb");
747 let txn = db.begin_write().expect("begin write");
748 {
749 let mut table = txn.open_table(manifest).expect("open marker manifest");
750 table
751 .insert("current", b"old-format".as_slice())
752 .expect("insert");
753 }
754 txn.commit().expect("commit");
755 }
756
757 fn create_future_marker_file(path: &Path) {
758 let manifest: redb::TableDefinition<&str, &[u8]> =
759 redb::TableDefinition::new("marker_manifest");
760 let store_format: redb::TableDefinition<&str, u32> =
761 redb::TableDefinition::new("store_format");
762 let db = redb::Database::create(path).expect("create redb");
763 let txn = db.begin_write().expect("begin write");
764 {
765 let mut format_table = txn.open_table(store_format).expect("open store format");
766 format_table
767 .insert("codec", format::STORE_FORMAT_VERSION + 1)
768 .expect("insert format");
769 let mut manifest_table = txn.open_table(manifest).expect("open marker manifest");
770 manifest_table
771 .insert("current", b"future-format".as_slice())
772 .expect("insert manifest");
773 }
774 txn.commit().expect("commit");
775 }
776
777 fn assert_corrupted_regenerated<T: Debug>(result: Result<T, EventStoreError>) {
778 match result {
779 Err(EventStoreError::Corrupted(msg)) => {
780 assert!(msg.contains("regenerated"), "msg was: {msg}");
781 }
782 other => panic!("expected Corrupted regeneration error, was {other:?}"),
783 }
784 }
785
786 fn assert_corrupted<T: Debug>(result: Result<T, EventStoreError>) {
787 match result {
788 Err(EventStoreError::Corrupted(_)) => {}
789 other => panic!("expected Corrupted error, was {other:?}"),
790 }
791 }
792
793 #[rstest]
794 fn open_rejects_marker_store_without_format_marker(temp_dir: TempDir) {
795 let run_id = "1700000000-redb-old-format";
796 let path = marker_path(temp_dir.path(), "trader-001", run_id);
797 std::fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
798 create_pre_codec_marker_file(&path);
799
800 let mut backend = RedbMarkerBackend::new(&path);
801 assert_corrupted_regenerated(backend.open_run(manifest(run_id)));
802 assert_corrupted_regenerated(RedbMarkerBackend::open_read_only_file(&path));
803 }
804
805 #[rstest]
806 fn open_rejects_marker_store_with_future_format_marker(temp_dir: TempDir) {
807 let run_id = "1700000000-redb-future-format";
808 let path = marker_path(temp_dir.path(), "trader-001", run_id);
809 std::fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
810 create_future_marker_file(&path);
811
812 let mut backend = RedbMarkerBackend::new(&path);
813 assert_corrupted(backend.open_run(manifest(run_id)));
814 assert_corrupted(RedbMarkerBackend::open_read_only_file(&path));
815 }
816
817 #[rstest]
818 fn roundtrip_snapshots_dict_gaps_through_redb(temp_dir: TempDir) {
819 let run_id = "1700000000-redb-roundtrip";
820 let path = marker_path(temp_dir.path(), "trader-001", run_id);
821 let s2 = snapshot(2, 20);
822 let s1 = snapshot(1, 10);
823 let h2 = hifi(4);
824 let h1 = hifi(3);
825 let g2 = gap(20, 24);
826 let g1 = gap(5, 8);
827 let first_dict = dict(0, DataClass::Quote, "ETHUSDT.BINANCE");
828 let remap_dict = dict(0, DataClass::Trade, "BTCUSDT.BINANCE");
829 let second_dict = dict(1, DataClass::Trade, "BTCUSDT.BINANCE");
830
831 {
832 let mut backend = RedbMarkerBackend::new(&path);
833 backend.open_run(manifest(run_id)).expect("open run");
834 backend
835 .append_snapshot(&s2, compute_marker_hash(&s2))
836 .expect("append snapshot 2");
837 backend
838 .append_snapshot(&s1, compute_marker_hash(&s1))
839 .expect("append snapshot 1");
840 backend
841 .append_hifi(&h2, compute_hifi_hash(&h2))
842 .expect("append hifi 4");
843 backend
844 .append_hifi(&h1, compute_hifi_hash(&h1))
845 .expect("append hifi 3");
846 backend
847 .append_gap(&g2, compute_gap_hash(&g2))
848 .expect("append gap 2");
849 backend
850 .append_gap(&g1, compute_gap_hash(&g1))
851 .expect("append gap 1");
852 backend
853 .put_dict(&first_dict, compute_dict_hash(&first_dict))
854 .expect("put dict 0");
855 backend
856 .put_dict(&remap_dict, compute_dict_hash(&remap_dict))
857 .expect("re-put dict 0");
858 backend
859 .put_dict(&second_dict, compute_dict_hash(&second_dict))
860 .expect("put dict 1");
861 }
862
863 let mut reopened = RedbMarkerBackend::new(&path);
864 reopened.open_run(manifest(run_id)).expect("reopen run");
865
866 assert_eq!(
867 reopened.scan_snapshots().expect("scan snapshots"),
868 vec![s1, s2]
869 );
870 assert_eq!(reopened.scan_hifi().expect("scan hifi"), vec![h1, h2]);
871 assert_eq!(reopened.scan_gaps().expect("scan gaps"), vec![g1, g2]);
872 assert_eq!(
873 reopened.scan_dict().expect("scan dict"),
874 vec![first_dict, second_dict]
875 );
876 }
877
878 #[rstest]
879 fn open_running_then_reopen_reports_crashed(temp_dir: TempDir) {
880 let run_id = "1700000000-redb-crash";
881 let path = marker_path(temp_dir.path(), "trader-001", run_id);
882 let s1 = snapshot(1, 10);
883
884 {
885 let mut backend = RedbMarkerBackend::new(&path);
886 backend.open_run(manifest(run_id)).expect("open run");
887 backend
888 .append_snapshot(&s1, compute_marker_hash(&s1))
889 .expect("append snapshot");
890 }
891
892 let mut reopened = RedbMarkerBackend::new(&path);
893 reopened.open_run(manifest(run_id)).expect("reopen run");
894
895 assert_eq!(
896 reopened.manifest().expect("manifest").status,
897 RunStatus::CrashedRecovered
898 );
899 assert_eq!(reopened.scan_snapshots().expect("scan snapshots"), vec![s1]);
900 }
901
902 #[rstest]
903 fn read_only_marker_backend_rejects_writes_without_recovery_mutation(temp_dir: TempDir) {
904 let run_id = "1700000000-redb-read-only-running";
905 let path = marker_path(temp_dir.path(), "trader-001", run_id);
906 let s1 = snapshot(1, 10);
907
908 {
909 let mut backend = RedbMarkerBackend::new(&path);
910 backend.open_run(manifest(run_id)).expect("open run");
911 backend
912 .append_snapshot(&s1, compute_marker_hash(&s1))
913 .expect("append snapshot");
914 }
915
916 let mut read_only =
917 RedbMarkerBackend::open_read_only_file(&path).expect("open read-only marker file");
918 let s2 = snapshot(2, 11);
919
920 assert_eq!(
921 read_only.manifest().expect("manifest").status,
922 RunStatus::Running,
923 );
924 assert_closed(read_only.append_snapshot(&s2, compute_marker_hash(&s2)));
925 assert_closed(read_only.seal(RunStatus::Ended));
926 assert_eq!(
927 read_only.scan_snapshots().expect("scan snapshots"),
928 vec![s1]
929 );
930 }
931
932 #[rstest]
933 fn seal_persists_manifest_counts(temp_dir: TempDir) {
934 let run_id = "1700000000-redb-seal";
935 let path = marker_path(temp_dir.path(), "trader-001", run_id);
936 let s1 = snapshot(1, 10);
937 let h1 = hifi(2);
938 let g1 = gap(3, 5);
939 let d1 = dict(0, DataClass::Quote, "ETHUSDT.BINANCE");
940
941 {
942 let mut backend = RedbMarkerBackend::new(&path);
943 backend.open_run(manifest(run_id)).expect("open run");
944 backend
945 .append_snapshot(&s1, compute_marker_hash(&s1))
946 .expect("append snapshot");
947 backend
948 .append_hifi(&h1, compute_hifi_hash(&h1))
949 .expect("append hifi");
950 backend
951 .append_gap(&g1, compute_gap_hash(&g1))
952 .expect("append gap");
953 backend
954 .put_dict(&d1, compute_dict_hash(&d1))
955 .expect("put dict");
956 backend.seal(RunStatus::Ended).expect("seal");
957 }
958
959 let mut reopened = RedbMarkerBackend::new(&path);
960 reopened
961 .open_run(manifest(run_id))
962 .expect("reopen sealed run");
963 let persisted = reopened.manifest().expect("manifest");
964
965 assert_eq!(persisted.status, RunStatus::Ended);
966 assert_eq!(persisted.snapshot_count, 1);
967 assert_eq!(persisted.hifi_count, 1);
968 assert_eq!(persisted.gap_count, 1);
969 assert_eq!(persisted.dict_count, 1);
970 }
971
972 #[rstest]
973 fn unsealed_appends_persist_manifest_counts_for_verifier(temp_dir: TempDir) {
974 let run_id = "1700000000-redb-unsealed-counts";
975 let path = marker_path(temp_dir.path(), "trader-001", run_id);
976 let s1 = snapshot(1, 1);
977 let h1 = hifi(2);
978 let g1 = gap(3, 3);
979 let first_dict = dict(0, DataClass::Quote, "ETHUSDT.BINANCE");
980 let remap_dict = dict(0, DataClass::Trade, "BTCUSDT.BINANCE");
981 let second_dict = dict(1, DataClass::Trade, "BTCUSDT.BINANCE");
982
983 {
984 let mut backend = RedbMarkerBackend::new(&path);
985 backend.open_run(manifest(run_id)).expect("open run");
986 backend
987 .append_snapshot(&s1, compute_marker_hash(&s1))
988 .expect("append snapshot");
989 backend
990 .append_hifi(&h1, compute_hifi_hash(&h1))
991 .expect("append hifi");
992 backend
993 .append_gap(&g1, compute_gap_hash(&g1))
994 .expect("append gap");
995 backend
996 .put_dict(&first_dict, compute_dict_hash(&first_dict))
997 .expect("put dict 0");
998 backend
999 .put_dict(&remap_dict, compute_dict_hash(&remap_dict))
1000 .expect("re-put dict 0");
1001 backend
1002 .put_dict(&second_dict, compute_dict_hash(&second_dict))
1003 .expect("put dict 1");
1004 }
1005
1006 let read_only =
1007 RedbMarkerBackend::open_read_only_file(&path).expect("open read-only marker file");
1008 let persisted = read_only.manifest().expect("manifest");
1009 let report = MarkerVerifier::scan(&read_only, 42).expect("scan");
1010
1011 assert_eq!(persisted.status, RunStatus::Running);
1012 assert_eq!(persisted.snapshot_count, 1);
1013 assert_eq!(persisted.hifi_count, 1);
1014 assert_eq!(persisted.gap_count, 1);
1015 assert_eq!(persisted.dict_count, 2);
1016 assert_eq!(report.snapshots_scanned, 1);
1017 assert_eq!(report.hifi_scanned, 1);
1018 assert_eq!(report.gaps_scanned, 1);
1019 assert_eq!(report.dict_entries_scanned, 2);
1020 assert!(report.is_clean(), "findings was: {:?}", report.findings);
1021 }
1022
1023 #[rstest]
1024 fn sealed_backend_rejects_writes(temp_dir: TempDir) {
1025 let run_id = "1700000000-redb-sealed-writes";
1026 let path = marker_path(temp_dir.path(), "trader-001", run_id);
1027 let mut backend = RedbMarkerBackend::new(&path);
1028 backend.open_run(manifest(run_id)).expect("open run");
1029 backend.seal(RunStatus::Ended).expect("seal");
1030
1031 let s1 = snapshot(1, 10);
1032 let h1 = hifi(2);
1033 let g1 = gap(3, 5);
1034 let d1 = dict(0, DataClass::Quote, "ETHUSDT.BINANCE");
1035
1036 assert_closed(backend.append_snapshot(&s1, compute_marker_hash(&s1)));
1037 assert_closed(backend.append_hifi(&h1, compute_hifi_hash(&h1)));
1038 assert_closed(backend.append_gap(&g1, compute_gap_hash(&g1)));
1039 assert_closed(backend.put_dict(&d1, compute_dict_hash(&d1)));
1040
1041 let persisted = backend.manifest().expect("manifest");
1042 assert_eq!(persisted.status, RunStatus::Ended);
1043 assert_eq!(persisted.snapshot_count, 0);
1044 assert_eq!(persisted.hifi_count, 0);
1045 assert_eq!(persisted.gap_count, 0);
1046 assert_eq!(persisted.dict_count, 0);
1047 }
1048
1049 #[rstest]
1050 fn crash_recovered_backend_rejects_writes(temp_dir: TempDir) {
1051 let run_id = "1700000000-redb-crash-closed";
1052 let path = marker_path(temp_dir.path(), "trader-001", run_id);
1053 let s1 = snapshot(1, 10);
1054 let s2 = snapshot(2, 20);
1055
1056 {
1057 let mut backend = RedbMarkerBackend::new(&path);
1058 backend.open_run(manifest(run_id)).expect("open run");
1059 backend
1060 .append_snapshot(&s1, compute_marker_hash(&s1))
1061 .expect("append snapshot");
1062 }
1063
1064 let mut reopened = RedbMarkerBackend::new(&path);
1065 reopened.open_run(manifest(run_id)).expect("reopen run");
1066
1067 assert_eq!(
1068 reopened.manifest().expect("manifest").status,
1069 RunStatus::CrashedRecovered
1070 );
1071 assert_closed(reopened.append_snapshot(&s2, compute_marker_hash(&s2)));
1072 assert_eq!(reopened.scan_snapshots().expect("scan snapshots"), vec![s1]);
1073 }
1074
1075 #[rstest]
1076 fn seal_running_is_rejected_and_does_not_close(temp_dir: TempDir) {
1077 let run_id = "1700000000-redb-running-seal";
1078 let path = marker_path(temp_dir.path(), "trader-001", run_id);
1079 let s1 = snapshot(1, 10);
1080 let mut backend = RedbMarkerBackend::new(&path);
1081 backend.open_run(manifest(run_id)).expect("open run");
1082
1083 assert_backend_message(
1084 backend.seal(RunStatus::Running),
1085 "seal status must be a terminal state, was Running",
1086 );
1087 backend
1088 .append_snapshot(&s1, compute_marker_hash(&s1))
1089 .expect("append after rejected seal");
1090 backend.seal(RunStatus::Ended).expect("seal ended");
1091
1092 let persisted = backend.manifest().expect("manifest");
1093 assert_eq!(persisted.status, RunStatus::Ended);
1094 assert_eq!(persisted.snapshot_count, 1);
1095 }
1096
1097 #[rstest]
1098 fn unopened_backend_reports_backend_error(temp_dir: TempDir) {
1099 let path = marker_path(temp_dir.path(), "trader-001", "1700000000-redb-unopened");
1100 let mut backend = RedbMarkerBackend::new(&path);
1101 let s1 = snapshot(1, 10);
1102 let h1 = hifi(2);
1103 let g1 = gap(3, 5);
1104 let d1 = dict(0, DataClass::Quote, "ETHUSDT.BINANCE");
1105
1106 assert_backend_message(backend.current_path(), "no marker run open");
1107 assert_backend_message(backend.manifest(), "no marker run open");
1108 assert_backend_message(backend.scan_snapshots(), "no marker run open");
1109 assert_backend_message(backend.scan_hifi(), "no marker run open");
1110 assert_backend_message(backend.scan_gaps(), "no marker run open");
1111 assert_backend_message(backend.scan_dict(), "no marker run open");
1112 assert_backend_message(backend.seal(RunStatus::Ended), "no marker run open");
1113 assert_backend_message(
1114 backend.append_snapshot(&s1, compute_marker_hash(&s1)),
1115 "no marker run open",
1116 );
1117 assert_backend_message(
1118 backend.append_hifi(&h1, compute_hifi_hash(&h1)),
1119 "no marker run open",
1120 );
1121 assert_backend_message(
1122 backend.append_gap(&g1, compute_gap_hash(&g1)),
1123 "no marker run open",
1124 );
1125 assert_backend_message(
1126 backend.put_dict(&d1, compute_dict_hash(&d1)),
1127 "no marker run open",
1128 );
1129 }
1130}