1use std::fmt::Debug;
19
20use indexmap::IndexMap;
21use serde::{Deserialize, Serialize};
22
23use crate::{
24 error::EventStoreError,
25 manifest::{RunId, RunStatus},
26 markers::{DataClass, DataCursorSnapshot, HiFiMarker, MarkerGap, StreamDictEntry, StreamSlot},
27};
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36pub struct MarkerManifest {
37 pub run_id: RunId,
39 pub enabled_classes: Vec<DataClass>,
41 pub high_fidelity: bool,
43 pub snapshot_count: u64,
45 pub hifi_count: u64,
47 pub gap_count: u64,
49 pub dict_count: u64,
51 pub status: RunStatus,
53}
54
55impl MarkerManifest {
56 #[must_use]
58 pub const fn is_sealed(&self) -> bool {
59 !matches!(self.status, RunStatus::Running)
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65pub struct StoredMarkerRecord<T> {
66 pub record: T,
68 pub hash: [u8; 32],
70}
71
72pub trait MarkerBackend: Debug + Send {
80 fn open_run(&mut self, manifest: MarkerManifest) -> Result<(), EventStoreError>;
89
90 fn append_snapshot(
97 &mut self,
98 snapshot: &DataCursorSnapshot,
99 hash: [u8; 32],
100 ) -> Result<(), EventStoreError>;
101
102 fn append_hifi(&mut self, marker: &HiFiMarker, hash: [u8; 32]) -> Result<(), EventStoreError>;
108
109 fn append_gap(&mut self, gap: &MarkerGap, hash: [u8; 32]) -> Result<(), EventStoreError>;
116
117 fn put_dict(&mut self, entry: &StreamDictEntry, hash: [u8; 32]) -> Result<(), EventStoreError>;
126
127 fn scan_snapshots(&self) -> Result<Vec<DataCursorSnapshot>, EventStoreError>;
134
135 fn scan_snapshot_records(
143 &self,
144 ) -> Result<Option<Vec<StoredMarkerRecord<DataCursorSnapshot>>>, EventStoreError> {
145 Ok(None)
146 }
147
148 fn scan_hifi(&self) -> Result<Vec<HiFiMarker>, EventStoreError>;
154
155 fn scan_hifi_records(
163 &self,
164 ) -> Result<Option<Vec<StoredMarkerRecord<HiFiMarker>>>, EventStoreError> {
165 Ok(None)
166 }
167
168 fn scan_gaps(&self) -> Result<Vec<MarkerGap>, EventStoreError>;
174
175 fn scan_gap_records(
183 &self,
184 ) -> Result<Option<Vec<StoredMarkerRecord<MarkerGap>>>, EventStoreError> {
185 Ok(None)
186 }
187
188 fn scan_dict(&self) -> Result<Vec<StreamDictEntry>, EventStoreError>;
194
195 fn scan_dict_records(
203 &self,
204 ) -> Result<Option<Vec<StoredMarkerRecord<StreamDictEntry>>>, EventStoreError> {
205 Ok(None)
206 }
207
208 fn seal(&mut self, status: RunStatus) -> Result<(), EventStoreError>;
215
216 fn manifest(&self) -> Result<MarkerManifest, EventStoreError>;
222}
223
224#[derive(Debug, Default)]
234pub struct MemoryMarkerBackend {
235 state: Option<RunState>,
236}
237
238#[derive(Debug)]
239struct RunState {
240 manifest: MarkerManifest,
241 snapshots: Vec<(DataCursorSnapshot, [u8; 32])>,
242 hifi: Vec<(HiFiMarker, [u8; 32])>,
243 gaps: Vec<(MarkerGap, [u8; 32])>,
244 dict: IndexMap<StreamSlot, (StreamDictEntry, [u8; 32])>,
245}
246
247impl MemoryMarkerBackend {
248 #[must_use]
250 pub fn new() -> Self {
251 Self::default()
252 }
253
254 fn state(&self) -> Result<&RunState, EventStoreError> {
255 self.state
256 .as_ref()
257 .ok_or_else(|| EventStoreError::Backend("no run open".to_string()))
258 }
259
260 fn state_mut(&mut self) -> Result<&mut RunState, EventStoreError> {
261 self.state
262 .as_mut()
263 .ok_or_else(|| EventStoreError::Backend("no run open".to_string()))
264 }
265
266 fn writable_state(&mut self) -> Result<&mut RunState, EventStoreError> {
267 let state = self.state_mut()?;
268 if state.manifest.is_sealed() {
269 return Err(EventStoreError::Closed);
270 }
271 Ok(state)
272 }
273}
274
275impl MarkerBackend for MemoryMarkerBackend {
276 fn open_run(&mut self, mut manifest: MarkerManifest) -> Result<(), EventStoreError> {
277 manifest.status = RunStatus::Running;
280 manifest.snapshot_count = 0;
281 manifest.hifi_count = 0;
282 manifest.gap_count = 0;
283 manifest.dict_count = 0;
284
285 self.state = Some(RunState {
286 manifest,
287 snapshots: Vec::new(),
288 hifi: Vec::new(),
289 gaps: Vec::new(),
290 dict: IndexMap::new(),
291 });
292 Ok(())
293 }
294
295 fn append_snapshot(
296 &mut self,
297 snapshot: &DataCursorSnapshot,
298 hash: [u8; 32],
299 ) -> Result<(), EventStoreError> {
300 let state = self.writable_state()?;
301 state.snapshots.push((snapshot.clone(), hash));
302 state.manifest.snapshot_count = state.snapshots.len() as u64;
303 Ok(())
304 }
305
306 fn append_hifi(&mut self, marker: &HiFiMarker, hash: [u8; 32]) -> Result<(), EventStoreError> {
307 let state = self.writable_state()?;
308 state.hifi.push((marker.clone(), hash));
309 state.manifest.hifi_count = state.hifi.len() as u64;
310 Ok(())
311 }
312
313 fn append_gap(&mut self, gap: &MarkerGap, hash: [u8; 32]) -> Result<(), EventStoreError> {
314 let state = self.writable_state()?;
315 state.gaps.push((gap.clone(), hash));
316 state.manifest.gap_count = state.gaps.len() as u64;
317 Ok(())
318 }
319
320 fn put_dict(&mut self, entry: &StreamDictEntry, hash: [u8; 32]) -> Result<(), EventStoreError> {
321 let state = self.writable_state()?;
322 state
326 .dict
327 .entry(entry.slot)
328 .or_insert_with(|| (entry.clone(), hash));
329 state.manifest.dict_count = state.dict.len() as u64;
330 Ok(())
331 }
332
333 fn scan_snapshots(&self) -> Result<Vec<DataCursorSnapshot>, EventStoreError> {
334 let out = self
335 .scan_snapshot_records()?
336 .unwrap_or_default()
337 .into_iter()
338 .map(|stored| stored.record)
339 .collect();
340 Ok(out)
341 }
342
343 fn scan_snapshot_records(
344 &self,
345 ) -> Result<Option<Vec<StoredMarkerRecord<DataCursorSnapshot>>>, EventStoreError> {
346 let mut out: Vec<StoredMarkerRecord<DataCursorSnapshot>> = self
347 .state()?
348 .snapshots
349 .iter()
350 .map(|(record, hash)| StoredMarkerRecord {
351 record: record.clone(),
352 hash: *hash,
353 })
354 .collect();
355 out.sort_by_key(|stored| stored.record.marker_seq);
356 Ok(Some(out))
357 }
358
359 fn scan_hifi(&self) -> Result<Vec<HiFiMarker>, EventStoreError> {
360 let out = self
361 .scan_hifi_records()?
362 .unwrap_or_default()
363 .into_iter()
364 .map(|stored| stored.record)
365 .collect();
366 Ok(out)
367 }
368
369 fn scan_hifi_records(
370 &self,
371 ) -> Result<Option<Vec<StoredMarkerRecord<HiFiMarker>>>, EventStoreError> {
372 let mut out: Vec<StoredMarkerRecord<HiFiMarker>> = self
373 .state()?
374 .hifi
375 .iter()
376 .map(|(record, hash)| StoredMarkerRecord {
377 record: record.clone(),
378 hash: *hash,
379 })
380 .collect();
381 out.sort_by_key(|stored| stored.record.marker_seq);
382 Ok(Some(out))
383 }
384
385 fn scan_gaps(&self) -> Result<Vec<MarkerGap>, EventStoreError> {
386 let out = self
387 .scan_gap_records()?
388 .unwrap_or_default()
389 .into_iter()
390 .map(|stored| stored.record)
391 .collect();
392 Ok(out)
393 }
394
395 fn scan_gap_records(
396 &self,
397 ) -> Result<Option<Vec<StoredMarkerRecord<MarkerGap>>>, EventStoreError> {
398 let mut out: Vec<StoredMarkerRecord<MarkerGap>> = self
399 .state()?
400 .gaps
401 .iter()
402 .map(|(record, hash)| StoredMarkerRecord {
403 record: record.clone(),
404 hash: *hash,
405 })
406 .collect();
407 out.sort_by_key(|stored| stored.record.from_marker_seq);
408 Ok(Some(out))
409 }
410
411 fn scan_dict(&self) -> Result<Vec<StreamDictEntry>, EventStoreError> {
412 let out = self
413 .scan_dict_records()?
414 .unwrap_or_default()
415 .into_iter()
416 .map(|stored| stored.record)
417 .collect();
418 Ok(out)
419 }
420
421 fn scan_dict_records(
422 &self,
423 ) -> Result<Option<Vec<StoredMarkerRecord<StreamDictEntry>>>, EventStoreError> {
424 let mut out: Vec<StoredMarkerRecord<StreamDictEntry>> = self
425 .state()?
426 .dict
427 .values()
428 .map(|(record, hash)| StoredMarkerRecord {
429 record: record.clone(),
430 hash: *hash,
431 })
432 .collect();
433 out.sort_by_key(|stored| stored.record.slot);
434 Ok(Some(out))
435 }
436
437 fn seal(&mut self, status: RunStatus) -> Result<(), EventStoreError> {
438 let state = self.state_mut()?;
439
440 if matches!(status, RunStatus::Running) {
443 return Err(EventStoreError::Backend(
444 "seal status must be a terminal state, was Running".to_string(),
445 ));
446 }
447
448 if state.manifest.is_sealed() {
449 return Err(EventStoreError::Closed);
450 }
451
452 state.manifest.status = status;
453 Ok(())
454 }
455
456 fn manifest(&self) -> Result<MarkerManifest, EventStoreError> {
457 Ok(self.state()?.manifest.clone())
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use nautilus_core::UnixNanos;
464 use rstest::{fixture, rstest};
465
466 use super::*;
467 use crate::markers::{
468 MarkerGapReason, StreamCursor, compute_dict_hash, compute_gap_hash, compute_hifi_hash,
469 compute_marker_hash,
470 };
471
472 fn manifest() -> MarkerManifest {
473 MarkerManifest {
474 run_id: "1700000000-aaaa1111".to_string(),
475 enabled_classes: vec![DataClass::Quote, DataClass::Trade],
476 high_fidelity: false,
477 snapshot_count: 0,
478 hifi_count: 0,
479 gap_count: 0,
480 dict_count: 0,
481 status: RunStatus::Running,
482 }
483 }
484
485 fn snapshot(marker_seq: u64, event_seq_before: u64) -> DataCursorSnapshot {
486 DataCursorSnapshot {
487 marker_seq,
488 event_seq_before,
489 ts_init: UnixNanos::from(1_700_000_000_000_000_000 + marker_seq),
490 advanced: vec![StreamCursor {
491 slot: 0,
492 ts_init_hi: UnixNanos::from(1_700_000_000_000_000_000 + marker_seq),
493 count: marker_seq,
494 }],
495 }
496 }
497
498 fn hifi(marker_seq: u64) -> HiFiMarker {
499 HiFiMarker {
500 marker_seq,
501 event_seq_before: 10,
502 slot: 0,
503 ts_event: UnixNanos::from(1),
504 ts_init: UnixNanos::from(2),
505 same_ts_ordinal: 0,
506 record_fingerprint: [0u8; 32],
507 }
508 }
509
510 #[fixture]
511 fn open_backend() -> MemoryMarkerBackend {
512 let mut backend = MemoryMarkerBackend::new();
513 backend.open_run(manifest()).expect("open run");
514 backend
515 }
516
517 #[rstest]
518 fn append_and_scan_snapshots_in_seq_order(mut open_backend: MemoryMarkerBackend) {
519 let s1 = snapshot(1, 10);
520 let s2 = snapshot(2, 20);
521 open_backend
522 .append_snapshot(&s1, compute_marker_hash(&s1))
523 .expect("append 1");
524 open_backend
525 .append_snapshot(&s2, compute_marker_hash(&s2))
526 .expect("append 2");
527
528 let scanned = open_backend.scan_snapshots().expect("scan");
529
530 assert_eq!(scanned, vec![s1, s2]);
531 }
532
533 #[rstest]
534 fn scans_return_marker_seq_order_regardless_of_append_order(
535 mut open_backend: MemoryMarkerBackend,
536 ) {
537 let s2 = snapshot(2, 20);
538 let s1 = snapshot(1, 10);
539 let m2 = hifi(4);
540 let m1 = hifi(3);
541 open_backend
543 .append_snapshot(&s2, compute_marker_hash(&s2))
544 .expect("snap 2");
545 open_backend
546 .append_snapshot(&s1, compute_marker_hash(&s1))
547 .expect("snap 1");
548 open_backend
549 .append_hifi(&m2, compute_hifi_hash(&m2))
550 .expect("hifi 4");
551 open_backend
552 .append_hifi(&m1, compute_hifi_hash(&m1))
553 .expect("hifi 3");
554
555 assert_eq!(open_backend.scan_snapshots().expect("snaps"), vec![s1, s2]);
556 assert_eq!(open_backend.scan_hifi().expect("hifi"), vec![m1, m2]);
557 }
558
559 #[rstest]
560 fn append_and_scan_hifi(mut open_backend: MemoryMarkerBackend) {
561 let m1 = hifi(1);
562 let m2 = hifi(2);
563 open_backend
564 .append_hifi(&m1, compute_hifi_hash(&m1))
565 .expect("hifi 1");
566 open_backend
567 .append_hifi(&m2, compute_hifi_hash(&m2))
568 .expect("hifi 2");
569
570 let scanned = open_backend.scan_hifi().expect("scan hifi");
571
572 assert_eq!(scanned, vec![m1, m2]);
573 }
574
575 #[rstest]
576 fn dict_entries_are_write_once_by_slot(mut open_backend: MemoryMarkerBackend) {
577 let first = StreamDictEntry {
578 slot: 0,
579 data_cls: DataClass::Quote,
580 identifier: "ETHUSDT.BINANCE".to_string(),
581 };
582 let remap = StreamDictEntry {
584 slot: 0,
585 data_cls: DataClass::Trade,
586 identifier: "BTCUSDT.BINANCE".to_string(),
587 };
588 let other = StreamDictEntry {
589 slot: 1,
590 data_cls: DataClass::Trade,
591 identifier: "BTCUSDT.BINANCE".to_string(),
592 };
593
594 open_backend
595 .put_dict(&first, compute_dict_hash(&first))
596 .expect("put 0");
597 open_backend
598 .put_dict(&remap, compute_dict_hash(&remap))
599 .expect("re-put 0");
600 open_backend
601 .put_dict(&other, compute_dict_hash(&other))
602 .expect("put 1");
603
604 let dict = open_backend.scan_dict().expect("scan dict");
605
606 assert_eq!(dict, vec![first, other]);
607 assert_eq!(open_backend.manifest().expect("manifest").dict_count, 2);
609 }
610
611 #[rstest]
612 fn append_gap_and_scan_gaps(mut open_backend: MemoryMarkerBackend) {
613 let g1 = MarkerGap {
614 from_marker_seq: 5,
615 to_marker_seq: 9,
616 reason: MarkerGapReason::Overflow,
617 };
618 let g2 = MarkerGap {
619 from_marker_seq: 20,
620 to_marker_seq: 20,
621 reason: MarkerGapReason::WriterClosed,
622 };
623 open_backend
624 .append_gap(&g1, compute_gap_hash(&g1))
625 .expect("gap 1");
626 open_backend
627 .append_gap(&g2, compute_gap_hash(&g2))
628 .expect("gap 2");
629
630 let gaps = open_backend.scan_gaps().expect("scan gaps");
631
632 assert_eq!(gaps, vec![g1, g2]);
633 }
634
635 #[rstest]
636 #[case::ended(RunStatus::Ended)]
637 #[case::crashed_recovered(RunStatus::CrashedRecovered)]
638 #[case::quarantined(RunStatus::Quarantined)]
639 fn seal_sets_manifest_status(mut open_backend: MemoryMarkerBackend, #[case] status: RunStatus) {
640 open_backend.seal(status).expect("seal");
641
642 let m = open_backend.manifest().expect("manifest");
643 assert_eq!(m.status, status);
644 assert!(m.is_sealed());
645 }
646
647 #[rstest]
648 fn open_run_normalizes_status_and_zeroes_counts() {
649 let mut backend = MemoryMarkerBackend::new();
650 let mut stale = manifest();
651 stale.status = RunStatus::Ended;
652 stale.snapshot_count = 99;
653 stale.hifi_count = 99;
654 stale.gap_count = 99;
655 stale.dict_count = 99;
656
657 backend.open_run(stale).expect("open");
658
659 let opened = backend.manifest().expect("manifest");
660 assert_eq!(opened.status, RunStatus::Running);
661 assert_eq!(opened.snapshot_count, 0);
662 assert_eq!(opened.hifi_count, 0);
663 assert_eq!(opened.gap_count, 0);
664 assert_eq!(opened.dict_count, 0);
665 assert_eq!(
667 opened.enabled_classes,
668 vec![DataClass::Quote, DataClass::Trade]
669 );
670 assert!(!opened.high_fidelity);
671 }
672
673 #[rstest]
674 fn manifest_counts_track_appends(mut open_backend: MemoryMarkerBackend) {
675 let snap = snapshot(1, 10);
676 let marker = hifi(2);
677 let gap = MarkerGap {
678 from_marker_seq: 3,
679 to_marker_seq: 4,
680 reason: MarkerGapReason::Overflow,
681 };
682 let entry = StreamDictEntry {
683 slot: 0,
684 data_cls: DataClass::Quote,
685 identifier: "ETHUSDT.BINANCE".to_string(),
686 };
687
688 open_backend
689 .append_snapshot(&snap, compute_marker_hash(&snap))
690 .expect("snap");
691 open_backend
692 .append_hifi(&marker, compute_hifi_hash(&marker))
693 .expect("hifi");
694 open_backend
695 .append_gap(&gap, compute_gap_hash(&gap))
696 .expect("gap");
697 open_backend
698 .put_dict(&entry, compute_dict_hash(&entry))
699 .expect("dict");
700
701 let m = open_backend.manifest().expect("manifest");
702 assert_eq!(m.snapshot_count, 1);
703 assert_eq!(m.hifi_count, 1);
704 assert_eq!(m.gap_count, 1);
705 assert_eq!(m.dict_count, 1);
706 }
707
708 #[rstest]
709 fn append_after_seal_returns_closed(mut open_backend: MemoryMarkerBackend) {
710 open_backend.seal(RunStatus::Ended).expect("seal");
711 let snap = snapshot(1, 10);
712
713 let err = open_backend
714 .append_snapshot(&snap, compute_marker_hash(&snap))
715 .expect_err("must reject");
716
717 assert!(matches!(err, EventStoreError::Closed));
718 }
719
720 #[rstest]
721 fn seal_rejects_running_status(mut open_backend: MemoryMarkerBackend) {
722 let err = open_backend
723 .seal(RunStatus::Running)
724 .expect_err("must reject");
725
726 match err {
727 EventStoreError::Backend(msg) => assert!(msg.contains("Running"), "msg was: {msg}"),
728 other => panic!("expected Backend, was {other:?}"),
729 }
730 assert!(!open_backend.manifest().expect("manifest").is_sealed());
731 }
732
733 #[rstest]
734 fn re_seal_returns_closed(mut open_backend: MemoryMarkerBackend) {
735 open_backend.seal(RunStatus::Ended).expect("seal");
736
737 let err = open_backend
738 .seal(RunStatus::Quarantined)
739 .expect_err("re-seal");
740
741 assert!(matches!(err, EventStoreError::Closed));
742 }
743
744 #[rstest]
745 #[case::append_snapshot("append_snapshot")]
746 #[case::append_hifi("append_hifi")]
747 #[case::append_gap("append_gap")]
748 #[case::put_dict("put_dict")]
749 #[case::scan_snapshots("scan_snapshots")]
750 #[case::scan_hifi("scan_hifi")]
751 #[case::scan_gaps("scan_gaps")]
752 #[case::scan_dict("scan_dict")]
753 #[case::seal("seal")]
754 #[case::manifest("manifest")]
755 fn methods_error_when_no_run_open(#[case] op: &str) {
756 let mut backend = MemoryMarkerBackend::new();
757 let snap = snapshot(1, 10);
758 let marker = hifi(1);
759 let gap = MarkerGap {
760 from_marker_seq: 1,
761 to_marker_seq: 2,
762 reason: MarkerGapReason::Overflow,
763 };
764 let entry = StreamDictEntry {
765 slot: 0,
766 data_cls: DataClass::Quote,
767 identifier: "ETHUSDT.BINANCE".to_string(),
768 };
769
770 let err = match op {
771 "append_snapshot" => backend
772 .append_snapshot(&snap, compute_marker_hash(&snap))
773 .unwrap_err(),
774 "append_hifi" => backend
775 .append_hifi(&marker, compute_hifi_hash(&marker))
776 .unwrap_err(),
777 "append_gap" => backend
778 .append_gap(&gap, compute_gap_hash(&gap))
779 .unwrap_err(),
780 "put_dict" => backend
781 .put_dict(&entry, compute_dict_hash(&entry))
782 .unwrap_err(),
783 "scan_snapshots" => backend.scan_snapshots().unwrap_err(),
784 "scan_hifi" => backend.scan_hifi().unwrap_err(),
785 "scan_gaps" => backend.scan_gaps().unwrap_err(),
786 "scan_dict" => backend.scan_dict().unwrap_err(),
787 "seal" => backend.seal(RunStatus::Ended).unwrap_err(),
788 "manifest" => backend.manifest().unwrap_err(),
789 _ => unreachable!(),
790 };
791
792 match err {
793 EventStoreError::Backend(msg) => assert!(msg.contains("no run open"), "msg was: {msg}"),
794 other => panic!("expected Backend, was {other:?}"),
795 }
796 }
797}