1use std::{collections::BTreeSet, fmt::Debug, path::Path};
41
42use crate::{
43 backend::{EventStore, IndexKind, RedbBackend},
44 entry::EventStoreEntry,
45 error::EventStoreError,
46 manifest::{RunId, RunManifest, RunStatus},
47};
48
49pub struct Verifier {
56 backend: Box<dyn EventStore>,
57}
58
59impl Debug for Verifier {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 f.debug_struct(stringify!(Verifier)).finish_non_exhaustive()
62 }
63}
64
65impl Verifier {
66 #[must_use]
68 pub fn new(backend: Box<dyn EventStore>) -> Self {
69 Self { backend }
70 }
71
72 pub fn open_redb(
84 base_dir: impl AsRef<Path>,
85 instance_id: &str,
86 run_id: &str,
87 ) -> Result<Self, VerifyError> {
88 let backend =
89 RedbBackend::open_sealed(base_dir.as_ref().to_path_buf(), instance_id, run_id)?;
90 Ok(Self {
91 backend: Box::new(backend),
92 })
93 }
94
95 pub fn open_redb_file(path: impl AsRef<Path>) -> Result<Self, VerifyError> {
106 let backend = RedbBackend::open_sealed_file(path.as_ref().to_path_buf())?;
107 Ok(Self {
108 backend: Box::new(backend),
109 })
110 }
111
112 #[must_use]
114 pub fn backend(&self) -> &dyn EventStore {
115 self.backend.as_ref()
116 }
117
118 pub fn verify(&self) -> Result<VerifyReport, VerifyError> {
131 let manifest = self.backend.manifest()?;
132 let high_watermark = self.backend.high_watermark()?;
133
134 let mut findings = Vec::new();
135 let scan = self.scan_entries(high_watermark, &mut findings)?;
136
137 self.cross_check_indices(&scan, &mut findings)?;
138 check_snapshot_anchor(self.backend.as_ref(), high_watermark, &mut findings)?;
139 validate_manifest(&manifest, high_watermark, &scan, &mut findings);
140
141 Ok(VerifyReport {
142 run_id: manifest.run_id.clone(),
143 status: manifest.status,
144 high_watermark,
145 entries_scanned: scan.scanned,
146 findings,
147 })
148 }
149
150 fn scan_entries(
151 &self,
152 high_watermark: u64,
153 findings: &mut Vec<VerifyFinding>,
154 ) -> Result<EntryScan, VerifyError> {
155 let mut scanned: u64 = 0;
156 let mut min_ts: Option<u64> = None;
157 let mut max_ts: Option<u64> = None;
158 let mut clean_seqs: BTreeSet<u64> = BTreeSet::new();
159 let mut corrupted_seqs: BTreeSet<u64> = BTreeSet::new();
160 let mut gap_cursor: Option<u64> = None;
161
162 for seq in 1..=high_watermark {
163 match self.backend.scan_seq(seq) {
164 Ok(Some(entry)) => {
165 flush_pending_gap(seq, &mut gap_cursor, findings);
166
167 if entry.seq != seq {
174 findings.push(VerifyFinding::SeqMismatch {
175 table_key: seq,
176 embedded_seq: entry.seq,
177 });
178 corrupted_seqs.insert(seq);
179 scanned += 1;
180 continue;
181 }
182 record_entry(&entry, &mut min_ts, &mut max_ts);
183 clean_seqs.insert(seq);
184 scanned += 1;
185 }
186 Ok(None) | Err(EventStoreError::Gap { .. }) => {
187 extend_pending_gap(seq, &mut gap_cursor);
188 }
189 Err(scan_err) => {
190 let finding = match scan_err {
191 EventStoreError::HashMismatch { seq: bad } => {
192 VerifyFinding::HashMismatch { seq: bad }
193 }
194 EventStoreError::SeqMismatch {
195 table_key,
196 embedded_seq,
197 } => VerifyFinding::SeqMismatch {
198 table_key,
199 embedded_seq,
200 },
201 EventStoreError::Corrupted(reason) => {
202 VerifyFinding::Undecodable { seq, reason }
203 }
204 other => return Err(VerifyError::Backend(other)),
205 };
206 flush_pending_gap(seq, &mut gap_cursor, findings);
207 findings.push(finding);
208 corrupted_seqs.insert(seq);
209 scanned += 1;
210 }
211 }
212 }
213
214 flush_pending_gap(high_watermark + 1, &mut gap_cursor, findings);
215
216 Ok(EntryScan {
217 scanned,
218 min_ts,
219 max_ts,
220 clean_seqs,
221 corrupted_seqs,
222 })
223 }
224
225 fn cross_check_indices(
226 &self,
227 scan: &EntryScan,
228 findings: &mut Vec<VerifyFinding>,
229 ) -> Result<(), VerifyError> {
230 for kind in [IndexKind::ClientOrderId, IndexKind::VenueOrderId] {
231 for (key, stored_seq) in self.backend.iter_index_keys(kind)? {
232 let drift = classify_target(stored_seq, scan);
233 if let Some(drift) = drift {
234 findings.push(VerifyFinding::IndexDrift { kind, key, drift });
235 }
236 }
237 }
238
239 Ok(())
240 }
241}
242
243#[derive(Debug)]
244struct EntryScan {
245 scanned: u64,
246 min_ts: Option<u64>,
247 max_ts: Option<u64>,
248 clean_seqs: BTreeSet<u64>,
249 corrupted_seqs: BTreeSet<u64>,
250}
251
252fn record_entry(entry: &EventStoreEntry, min_ts: &mut Option<u64>, max_ts: &mut Option<u64>) {
253 let ts = entry.ts_init.as_u64();
254 *min_ts = Some(min_ts.map_or(ts, |cur| cur.min(ts)));
255 *max_ts = Some(max_ts.map_or(ts, |cur| cur.max(ts)));
256}
257
258fn extend_pending_gap(seq: u64, gap_cursor: &mut Option<u64>) {
259 if gap_cursor.is_none() {
260 *gap_cursor = Some(seq);
261 }
262}
263
264fn flush_pending_gap(
265 next_seq: u64,
266 gap_cursor: &mut Option<u64>,
267 findings: &mut Vec<VerifyFinding>,
268) {
269 if let Some(start) = gap_cursor.take() {
270 findings.push(VerifyFinding::Gap {
271 range: GapRange {
272 from: start,
273 to: next_seq - 1,
274 },
275 });
276 }
277}
278
279fn classify_target(stored_seq: u64, scan: &EntryScan) -> Option<IndexDrift> {
280 if scan.clean_seqs.contains(&stored_seq) {
281 None
282 } else if scan.corrupted_seqs.contains(&stored_seq) {
283 Some(IndexDrift::TargetCorrupted { stored_seq })
284 } else {
285 Some(IndexDrift::DanglingTarget { stored_seq })
286 }
287}
288
289fn check_snapshot_anchor(
294 backend: &dyn EventStore,
295 high_watermark: u64,
296 findings: &mut Vec<VerifyFinding>,
297) -> Result<(), VerifyError> {
298 match backend.latest_snapshot_anchor() {
299 Ok(Some(anchor)) if anchor.high_watermark > high_watermark => {
300 findings.push(VerifyFinding::SnapshotAnchorInvalid {
301 reason: format!(
302 "snapshot anchor high_watermark {} exceeds durable high_watermark {high_watermark}",
303 anchor.high_watermark,
304 ),
305 });
306 }
307 Ok(_) => {}
308 Err(EventStoreError::Corrupted(msg)) => {
309 findings.push(VerifyFinding::SnapshotAnchorInvalid {
310 reason: format!("snapshot anchor unreadable: {msg}"),
311 });
312 }
313 Err(other) => return Err(VerifyError::Backend(other)),
314 }
315 Ok(())
316}
317
318fn validate_manifest(
319 manifest: &RunManifest,
320 high_watermark: u64,
321 scan: &EntryScan,
322 findings: &mut Vec<VerifyFinding>,
323) {
324 if manifest.high_watermark != high_watermark {
325 findings.push(VerifyFinding::ManifestMismatch {
326 kind: ManifestField::HighWatermark,
327 reason: format!(
328 "manifest high_watermark {} disagrees with durable high_watermark {high_watermark}",
329 manifest.high_watermark,
330 ),
331 });
332 }
333
334 if let Some(min_ts) = scan.min_ts
335 && manifest.start_ts_init.as_u64() > min_ts
336 {
337 findings.push(VerifyFinding::ManifestMismatch {
338 kind: ManifestField::StartTsInit,
339 reason: format!(
340 "manifest start_ts_init {} sits above earliest entry ts_init {min_ts}",
341 manifest.start_ts_init.as_u64(),
342 ),
343 });
344 }
345
346 if manifest.is_sealed() {
347 match (manifest.end_ts_init.map(|t| t.as_u64()), scan.max_ts) {
348 (Some(stored), Some(observed)) if stored != observed => {
349 findings.push(VerifyFinding::ManifestMismatch {
350 kind: ManifestField::EndTsInit,
351 reason: format!(
352 "manifest end_ts_init {stored} disagrees with last observed ts_init {observed}",
353 ),
354 });
355 }
356 (None, Some(observed)) => findings.push(VerifyFinding::ManifestMismatch {
357 kind: ManifestField::EndTsInit,
358 reason: format!(
359 "sealed manifest is missing end_ts_init while entries up to ts_init {observed} exist",
360 ),
361 }),
362 (Some(stored), None) => findings.push(VerifyFinding::ManifestMismatch {
363 kind: ManifestField::EndTsInit,
364 reason: format!(
365 "sealed manifest carries end_ts_init {stored} despite empty entry table",
366 ),
367 }),
368 _ => {}
369 }
370 }
371}
372
373#[derive(Debug, Clone, PartialEq, Eq)]
379pub struct VerifyReport {
380 pub run_id: RunId,
382 pub status: RunStatus,
384 pub high_watermark: u64,
386 pub entries_scanned: u64,
388 pub findings: Vec<VerifyFinding>,
390}
391
392impl VerifyReport {
393 #[must_use]
395 pub fn is_clean(&self) -> bool {
396 self.findings.is_empty()
397 }
398}
399
400#[derive(Debug, Clone, PartialEq, Eq)]
402pub enum VerifyFinding {
403 HashMismatch {
405 seq: u64,
407 },
408 Gap {
410 range: GapRange,
412 },
413 SeqMismatch {
421 table_key: u64,
423 embedded_seq: u64,
425 },
426 Undecodable {
430 seq: u64,
432 reason: String,
434 },
435 IndexDrift {
438 kind: IndexKind,
440 key: String,
442 drift: IndexDrift,
444 },
445 ManifestMismatch {
448 kind: ManifestField,
450 reason: String,
452 },
453 SnapshotAnchorInvalid {
456 reason: String,
458 },
459}
460
461#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
463pub struct GapRange {
464 pub from: u64,
466 pub to: u64,
468}
469
470#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
477pub enum IndexDrift {
478 DanglingTarget {
480 stored_seq: u64,
482 },
483 TargetCorrupted {
485 stored_seq: u64,
487 },
488}
489
490#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
492pub enum ManifestField {
493 HighWatermark,
495 StartTsInit,
497 EndTsInit,
500}
501
502#[derive(Debug, thiserror::Error)]
508pub enum VerifyError {
509 #[error("backend access failed: {0}")]
511 Backend(#[from] EventStoreError),
512}
513
514#[cfg(test)]
515mod tests {
516 use bytes::Bytes;
517 use indexmap::IndexMap;
518 use nautilus_core::UnixNanos;
519 use rstest::{fixture, rstest};
520 use ustr::Ustr;
521
522 use super::*;
523 use crate::{
524 backend::{AppendEntry, IndexKey, MemoryBackend, ScanDirection},
525 compute_entry_hash,
526 entry::Topic,
527 headers::Headers,
528 manifest::{RegisteredComponents, RunManifest, RunStatus},
529 };
530
531 fn manifest(run_id: &str) -> RunManifest {
532 RunManifest {
533 run_id: run_id.to_string(),
534 parent_run_id: None,
535 instance_id: "trader-001".to_string(),
536 binary_hash: "deadbeef".to_string(),
537 schema_version: 1,
538 crate_versions: "feedface".to_string(),
539 feature_flags: Vec::new(),
540 adapter_versions: IndexMap::new(),
541 config_hash: "cafebabe".to_string(),
542 registered_components: RegisteredComponents::default(),
543 seed: None,
544 start_ts_init: UnixNanos::from(0),
545 end_ts_init: None,
546 high_watermark: 0,
547 status: RunStatus::Running,
548 }
549 }
550
551 fn build_entry(seq: u64, headers: Headers, ts_init: u64) -> EventStoreEntry {
552 let topic: Topic = "exec.command.SubmitOrder".into();
553 let payload_type = Ustr::from("SubmitOrder");
554 let payload = Bytes::from_static(b"\x01\x02\x03\x04");
555 let ts_publish = UnixNanos::from(ts_init + 1);
556 let ts_init = UnixNanos::from(ts_init);
557 let hash = compute_entry_hash(
558 seq,
559 ts_init,
560 ts_publish,
561 topic.as_ref(),
562 payload_type.as_str(),
563 &payload,
564 &headers,
565 );
566
567 EventStoreEntry::new(
568 hash,
569 seq,
570 headers,
571 topic,
572 payload_type,
573 payload,
574 ts_init,
575 ts_publish,
576 )
577 }
578
579 fn append_with(seq: u64, ts_init: u64, index_keys: Vec<IndexKey>) -> AppendEntry {
580 AppendEntry::new(build_entry(seq, Headers::empty(), ts_init), index_keys)
581 }
582
583 struct ManifestOverrideBackend {
588 inner: MemoryBackend,
589 manifest_override: RunManifest,
590 high_watermark_override: Option<u64>,
591 }
592
593 impl ManifestOverrideBackend {
594 fn new(inner: MemoryBackend, manifest_override: RunManifest) -> Self {
595 Self {
596 inner,
597 manifest_override,
598 high_watermark_override: None,
599 }
600 }
601
602 fn with_high_watermark(mut self, hwm: u64) -> Self {
603 self.high_watermark_override = Some(hwm);
604 self
605 }
606 }
607
608 impl EventStore for ManifestOverrideBackend {
609 fn open_run(&mut self, m: RunManifest) -> Result<(), EventStoreError> {
610 self.inner.open_run(m)
611 }
612
613 fn append_batch(&mut self, entries: &[AppendEntry]) -> Result<u64, EventStoreError> {
614 self.inner.append_batch(entries)
615 }
616
617 fn scan_range(
618 &self,
619 from: u64,
620 to: u64,
621 direction: ScanDirection,
622 ) -> Result<Vec<EventStoreEntry>, EventStoreError> {
623 self.inner.scan_range(from, to, direction)
624 }
625
626 fn scan_seq(&self, seq: u64) -> Result<Option<EventStoreEntry>, EventStoreError> {
627 self.inner.scan_seq(seq)
628 }
629
630 fn lookup(&self, kind: IndexKind, key: &str) -> Result<Option<u64>, EventStoreError> {
631 self.inner.lookup(kind, key)
632 }
633
634 fn iter_index_keys(&self, kind: IndexKind) -> Result<Vec<(String, u64)>, EventStoreError> {
635 self.inner.iter_index_keys(kind)
636 }
637
638 fn seal(&mut self, status: RunStatus) -> Result<(), EventStoreError> {
639 self.inner.seal(status)
640 }
641
642 fn manifest(&self) -> Result<RunManifest, EventStoreError> {
643 Ok(self.manifest_override.clone())
644 }
645
646 fn high_watermark(&self) -> Result<u64, EventStoreError> {
647 if let Some(hwm) = self.high_watermark_override {
648 return Ok(hwm);
649 }
650 self.inner.high_watermark()
651 }
652 }
653
654 #[fixture]
655 fn open_backend() -> MemoryBackend {
656 let mut backend = MemoryBackend::new();
657 backend
658 .open_run(manifest("1700000000-aaaa1111"))
659 .expect("open run");
660 backend
661 }
662
663 fn verifier_for(backend: MemoryBackend) -> Verifier {
664 Verifier::new(Box::new(backend))
665 }
666
667 #[rstest]
668 fn clean_run_reports_no_findings(mut open_backend: MemoryBackend) {
669 open_backend
670 .append_batch(&[
671 append_with(1, 10, Vec::new()),
672 append_with(2, 11, Vec::new()),
673 append_with(3, 12, Vec::new()),
674 ])
675 .expect("append");
676 open_backend.seal(RunStatus::Ended).expect("seal");
677
678 let report = verifier_for(open_backend).verify().expect("verify");
679
680 assert!(report.is_clean(), "findings was: {:?}", report.findings);
684 assert_eq!(report.findings.len(), 0);
685 assert_eq!(report.high_watermark, 3);
686 assert_eq!(report.entries_scanned, 3);
687 assert_eq!(report.status, RunStatus::Ended);
688 }
689
690 #[rstest]
691 fn empty_run_reports_no_findings(mut open_backend: MemoryBackend) {
692 open_backend.seal(RunStatus::Ended).expect("seal");
693
694 let report = verifier_for(open_backend).verify().expect("verify");
695
696 assert!(report.is_clean(), "findings was: {:?}", report.findings);
697 assert_eq!(report.entries_scanned, 0);
698 }
699
700 #[rstest]
701 fn hash_mismatch_surfaces_per_seq(mut open_backend: MemoryBackend) {
702 open_backend
703 .append_batch(&[append_with(1, 10, Vec::new())])
704 .expect("append");
705 let mut tampered = build_entry(2, Headers::empty(), 11);
706 tampered.payload = Bytes::from_static(b"\xFF");
707 open_backend
708 .append_batch(&[AppendEntry::without_indices(tampered)])
709 .expect("append");
710 open_backend
711 .append_batch(&[append_with(3, 12, Vec::new())])
712 .expect("append");
713
714 let report = verifier_for(open_backend).verify().expect("verify");
715
716 assert!(
717 report
718 .findings
719 .iter()
720 .any(|f| matches!(f, VerifyFinding::HashMismatch { seq: 2 })),
721 "findings was: {:?}",
722 report.findings,
723 );
724 assert_eq!(report.entries_scanned, 3);
725 assert_eq!(report.high_watermark, 3);
726 }
727
728 #[rstest]
729 fn multiple_hash_mismatches_all_surface(mut open_backend: MemoryBackend) {
730 for seq in 1..=4u64 {
734 let mut entry = build_entry(seq, Headers::empty(), 10 + seq);
735 if seq == 2 || seq == 4 {
736 entry.payload = Bytes::from_static(b"\xFF");
737 }
738 open_backend
739 .append_batch(&[AppendEntry::without_indices(entry)])
740 .expect("append");
741 }
742
743 let report = verifier_for(open_backend).verify().expect("verify");
744
745 let mismatch_seqs: Vec<u64> = report
746 .findings
747 .iter()
748 .filter_map(|f| match f {
749 VerifyFinding::HashMismatch { seq } => Some(*seq),
750 _ => None,
751 })
752 .collect();
753 assert_eq!(mismatch_seqs, vec![2, 4]);
754 }
755
756 #[rstest]
757 fn client_order_id_index_clean_when_target_resolves(mut open_backend: MemoryBackend) {
758 open_backend
759 .append_batch(&[AppendEntry::new(
760 build_entry(1, Headers::empty(), 10),
761 vec![IndexKey::new(IndexKind::ClientOrderId, "O-1".to_string())],
762 )])
763 .expect("append");
764 open_backend.seal(RunStatus::Ended).expect("seal");
765
766 let report = verifier_for(open_backend).verify().expect("verify");
767
768 assert!(report.is_clean(), "findings was: {:?}", report.findings);
769 }
770
771 #[rstest]
772 #[case::client_order_id(IndexKind::ClientOrderId)]
773 #[case::venue_order_id(IndexKind::VenueOrderId)]
774 fn entity_index_target_corrupted_drift(
775 mut open_backend: MemoryBackend,
776 #[case] kind: IndexKind,
777 ) {
778 let mut tampered = build_entry(1, Headers::empty(), 10);
783 tampered.payload = Bytes::from_static(b"\xFF");
784 open_backend
785 .append_batch(&[AppendEntry::new(
786 tampered,
787 vec![IndexKey::new(kind, "K-1".to_string())],
788 )])
789 .expect("append");
790
791 let report = verifier_for(open_backend).verify().expect("verify");
792
793 assert!(
794 report.findings.iter().any(|f| matches!(
795 f,
796 VerifyFinding::IndexDrift {
797 kind: drift_kind,
798 drift: IndexDrift::TargetCorrupted { stored_seq: 1 },
799 ..
800 } if *drift_kind == kind
801 )),
802 "findings was: {:?}",
803 report.findings,
804 );
805 }
806
807 fn find_manifest_mismatch(findings: &[VerifyFinding], target: ManifestField) -> &str {
808 findings
809 .iter()
810 .find_map(|f| match f {
811 VerifyFinding::ManifestMismatch { kind, reason } if *kind == target => {
812 Some(reason.as_str())
813 }
814 _ => None,
815 })
816 .unwrap_or_else(|| {
817 panic!("expected ManifestMismatch({target:?}), findings was: {findings:?}")
818 })
819 }
820
821 #[rstest]
822 fn manifest_high_watermark_drift() {
823 let mut inner = MemoryBackend::new();
827 inner.open_run(manifest("run-hwm")).expect("open run");
828 inner
829 .append_batch(&[append_with(1, 10, Vec::new())])
830 .expect("append");
831 inner.seal(RunStatus::Ended).expect("seal");
832
833 let mut stale = inner.manifest().expect("manifest");
834 stale.high_watermark = 99;
835 let backend = ManifestOverrideBackend::new(inner, stale);
836
837 let report = Verifier::new(Box::new(backend)).verify().expect("verify");
838 let reason = find_manifest_mismatch(&report.findings, ManifestField::HighWatermark);
839
840 assert!(reason.contains("99"), "reason was: {reason}");
841 assert!(reason.contains('1'), "reason was: {reason}");
842 }
843
844 #[rstest]
845 fn manifest_end_ts_init_drift_when_sealed() {
846 let mut inner = MemoryBackend::new();
850 inner.open_run(manifest("run-end-ts")).expect("open run");
851 inner
852 .append_batch(&[
853 append_with(1, 10, Vec::new()),
854 append_with(2, 25, Vec::new()),
855 ])
856 .expect("append");
857 inner.seal(RunStatus::Ended).expect("seal");
858
859 let mut drifted = inner.manifest().expect("manifest");
860 drifted.end_ts_init = Some(UnixNanos::from(99));
861 let backend = ManifestOverrideBackend::new(inner, drifted);
862
863 let report = Verifier::new(Box::new(backend)).verify().expect("verify");
864 let reason = find_manifest_mismatch(&report.findings, ManifestField::EndTsInit);
865
866 assert!(reason.contains("99"), "reason was: {reason}");
867 assert!(reason.contains("25"), "reason was: {reason}");
868 }
869
870 #[rstest]
871 fn manifest_end_ts_init_missing_when_sealed_with_entries() {
872 let mut inner = MemoryBackend::new();
876 inner
877 .open_run(manifest("run-end-ts-missing"))
878 .expect("open run");
879 inner
880 .append_batch(&[append_with(1, 42, Vec::new())])
881 .expect("append");
882 inner.seal(RunStatus::Ended).expect("seal");
883
884 let mut drifted = inner.manifest().expect("manifest");
885 drifted.end_ts_init = None;
886 let backend = ManifestOverrideBackend::new(inner, drifted);
887
888 let report = Verifier::new(Box::new(backend)).verify().expect("verify");
889 let reason = find_manifest_mismatch(&report.findings, ManifestField::EndTsInit);
890
891 assert!(reason.contains("missing"), "reason was: {reason}");
892 assert!(reason.contains("42"), "reason was: {reason}");
893 }
894
895 #[rstest]
896 fn manifest_end_ts_init_set_on_sealed_empty_run() {
897 let mut inner = MemoryBackend::new();
901 inner
902 .open_run(manifest("run-end-ts-empty"))
903 .expect("open run");
904 inner.seal(RunStatus::Ended).expect("seal");
905
906 let mut drifted = inner.manifest().expect("manifest");
907 drifted.end_ts_init = Some(UnixNanos::from(77));
908 let backend = ManifestOverrideBackend::new(inner, drifted);
909
910 let report = Verifier::new(Box::new(backend)).verify().expect("verify");
911 let reason = find_manifest_mismatch(&report.findings, ManifestField::EndTsInit);
912
913 assert!(reason.contains("77"), "reason was: {reason}");
914 assert!(reason.contains("empty"), "reason was: {reason}");
915 }
916
917 #[rstest]
918 fn manifest_start_ts_init_drift() {
919 let mut inner = MemoryBackend::new();
923 inner.open_run(manifest("run-start-ts")).expect("open run");
924 inner
925 .append_batch(&[
926 append_with(1, 10, Vec::new()),
927 append_with(2, 25, Vec::new()),
928 ])
929 .expect("append");
930 inner.seal(RunStatus::Ended).expect("seal");
931
932 let mut drifted = inner.manifest().expect("manifest");
933 drifted.start_ts_init = UnixNanos::from(50);
934 let backend = ManifestOverrideBackend::new(inner, drifted);
935
936 let report = Verifier::new(Box::new(backend)).verify().expect("verify");
937 let reason = find_manifest_mismatch(&report.findings, ManifestField::StartTsInit);
938
939 assert!(reason.contains("50"), "reason was: {reason}");
940 assert!(reason.contains("10"), "reason was: {reason}");
941 }
942
943 #[rstest]
944 fn trailing_gap_surfaces_when_last_seqs_missing() {
945 let mut inner = MemoryBackend::new();
951 inner
952 .open_run(manifest("run-trailing-gap"))
953 .expect("open run");
954 inner
955 .append_batch(&[
956 append_with(1, 10, Vec::new()),
957 append_with(2, 11, Vec::new()),
958 append_with(3, 12, Vec::new()),
959 ])
960 .expect("append");
961 inner.seal(RunStatus::Ended).expect("seal");
962
963 let mut drifted = inner.manifest().expect("manifest");
964 drifted.high_watermark = 5;
965 let backend = ManifestOverrideBackend::new(inner, drifted).with_high_watermark(5);
968
969 let report = Verifier::new(Box::new(backend)).verify().expect("verify");
970
971 let gaps: Vec<GapRange> = report
972 .findings
973 .iter()
974 .filter_map(|f| match f {
975 VerifyFinding::Gap { range } => Some(*range),
976 _ => None,
977 })
978 .collect();
979 assert_eq!(gaps, vec![GapRange { from: 4, to: 5 }]);
980 assert_eq!(report.entries_scanned, 3);
981 assert_eq!(report.high_watermark, 5);
982 }
983
984 struct SeqRewriteBackend {
989 inner: MemoryBackend,
990 target_key: u64,
991 substitute: EventStoreEntry,
992 }
993
994 impl EventStore for SeqRewriteBackend {
995 fn open_run(&mut self, m: RunManifest) -> Result<(), EventStoreError> {
996 self.inner.open_run(m)
997 }
998 fn append_batch(&mut self, e: &[AppendEntry]) -> Result<u64, EventStoreError> {
999 self.inner.append_batch(e)
1000 }
1001 fn scan_range(
1002 &self,
1003 from: u64,
1004 to: u64,
1005 direction: ScanDirection,
1006 ) -> Result<Vec<EventStoreEntry>, EventStoreError> {
1007 self.inner.scan_range(from, to, direction)
1008 }
1009 fn scan_seq(&self, seq: u64) -> Result<Option<EventStoreEntry>, EventStoreError> {
1010 if seq == self.target_key {
1011 return Ok(Some(self.substitute.clone()));
1012 }
1013 self.inner.scan_seq(seq)
1014 }
1015 fn lookup(&self, kind: IndexKind, key: &str) -> Result<Option<u64>, EventStoreError> {
1016 self.inner.lookup(kind, key)
1017 }
1018 fn iter_index_keys(&self, kind: IndexKind) -> Result<Vec<(String, u64)>, EventStoreError> {
1019 self.inner.iter_index_keys(kind)
1020 }
1021 fn seal(&mut self, status: RunStatus) -> Result<(), EventStoreError> {
1022 self.inner.seal(status)
1023 }
1024 fn manifest(&self) -> Result<RunManifest, EventStoreError> {
1025 self.inner.manifest()
1026 }
1027 fn high_watermark(&self) -> Result<u64, EventStoreError> {
1028 self.inner.high_watermark()
1029 }
1030 }
1031
1032 struct UndecodableBackend {
1035 inner: MemoryBackend,
1036 target_key: u64,
1037 }
1038
1039 impl EventStore for UndecodableBackend {
1040 fn open_run(&mut self, m: RunManifest) -> Result<(), EventStoreError> {
1041 self.inner.open_run(m)
1042 }
1043 fn append_batch(&mut self, e: &[AppendEntry]) -> Result<u64, EventStoreError> {
1044 self.inner.append_batch(e)
1045 }
1046 fn scan_range(
1047 &self,
1048 from: u64,
1049 to: u64,
1050 direction: ScanDirection,
1051 ) -> Result<Vec<EventStoreEntry>, EventStoreError> {
1052 self.inner.scan_range(from, to, direction)
1053 }
1054 fn scan_seq(&self, seq: u64) -> Result<Option<EventStoreEntry>, EventStoreError> {
1055 if seq == self.target_key {
1056 return Err(EventStoreError::Corrupted(format!(
1057 "decode entry seq={seq}: bad length prefix",
1058 )));
1059 }
1060 self.inner.scan_seq(seq)
1061 }
1062 fn lookup(&self, kind: IndexKind, key: &str) -> Result<Option<u64>, EventStoreError> {
1063 self.inner.lookup(kind, key)
1064 }
1065 fn iter_index_keys(&self, kind: IndexKind) -> Result<Vec<(String, u64)>, EventStoreError> {
1066 self.inner.iter_index_keys(kind)
1067 }
1068 fn seal(&mut self, status: RunStatus) -> Result<(), EventStoreError> {
1069 self.inner.seal(status)
1070 }
1071 fn manifest(&self) -> Result<RunManifest, EventStoreError> {
1072 self.inner.manifest()
1073 }
1074 fn high_watermark(&self) -> Result<u64, EventStoreError> {
1075 self.inner.high_watermark()
1076 }
1077 }
1078
1079 #[rstest]
1080 fn undecodable_row_is_recorded_and_scan_continues() {
1081 let mut inner = MemoryBackend::new();
1084 inner
1085 .open_run(manifest("run-undecodable"))
1086 .expect("open run");
1087 inner
1088 .append_batch(&[
1089 append_with(1, 10, Vec::new()),
1090 AppendEntry::new(
1091 build_entry(2, Headers::empty(), 11),
1092 vec![IndexKey::new(IndexKind::ClientOrderId, "O-1".to_string())],
1093 ),
1094 append_with(3, 12, Vec::new()),
1095 ])
1096 .expect("append");
1097 inner.seal(RunStatus::Ended).expect("seal");
1098
1099 let backend = UndecodableBackend {
1100 inner,
1101 target_key: 2,
1102 };
1103
1104 let report = Verifier::new(Box::new(backend)).verify().expect("verify");
1105
1106 assert!(!report.is_clean());
1107 assert_eq!(report.entries_scanned, 3);
1108 assert_eq!(
1109 report.findings.len(),
1110 2,
1111 "findings was: {:?}",
1112 report.findings,
1113 );
1114 assert!(
1115 report
1116 .findings
1117 .iter()
1118 .any(|f| matches!(f, VerifyFinding::Undecodable { seq: 2, .. },)),
1119 "findings was: {:?}",
1120 report.findings,
1121 );
1122 assert!(
1123 report.findings.iter().any(|f| matches!(
1124 f,
1125 VerifyFinding::IndexDrift {
1126 drift: IndexDrift::TargetCorrupted { stored_seq: 2 },
1127 ..
1128 },
1129 )),
1130 "findings was: {:?}",
1131 report.findings,
1132 );
1133 }
1134
1135 #[rstest]
1136 fn seq_mismatch_surfaces_when_row_value_disagrees_with_key() {
1137 let mut inner = MemoryBackend::new();
1143 inner
1144 .open_run(manifest("run-seq-mismatch"))
1145 .expect("open run");
1146 inner
1147 .append_batch(&[
1148 append_with(1, 10, Vec::new()),
1149 append_with(2, 11, Vec::new()),
1150 append_with(3, 12, Vec::new()),
1151 ])
1152 .expect("append");
1153 inner.seal(RunStatus::Ended).expect("seal");
1154
1155 let substitute = build_entry(99, Headers::empty(), 11);
1156 let backend = SeqRewriteBackend {
1157 inner,
1158 target_key: 2,
1159 substitute,
1160 };
1161
1162 let report = Verifier::new(Box::new(backend)).verify().expect("verify");
1163
1164 assert!(
1165 report.findings.iter().any(|f| matches!(
1166 f,
1167 VerifyFinding::SeqMismatch {
1168 table_key: 2,
1169 embedded_seq: 99,
1170 }
1171 )),
1172 "findings was: {:?}",
1173 report.findings,
1174 );
1175 }
1176
1177 #[rstest]
1178 fn seq_mismatch_marks_target_corrupted_for_dependent_indices() {
1179 let mut inner = MemoryBackend::new();
1184 inner
1185 .open_run(manifest("run-seq-mismatch-idx"))
1186 .expect("open run");
1187 inner
1188 .append_batch(&[
1189 append_with(1, 10, Vec::new()),
1190 AppendEntry::new(
1191 build_entry(2, Headers::empty(), 11),
1192 vec![IndexKey::new(IndexKind::ClientOrderId, "O-1".to_string())],
1193 ),
1194 ])
1195 .expect("append");
1196 inner.seal(RunStatus::Ended).expect("seal");
1197
1198 let substitute = build_entry(99, Headers::empty(), 11);
1199 let backend = SeqRewriteBackend {
1200 inner,
1201 target_key: 2,
1202 substitute,
1203 };
1204
1205 let report = Verifier::new(Box::new(backend)).verify().expect("verify");
1206
1207 assert!(
1208 report.findings.iter().any(|f| matches!(
1209 f,
1210 VerifyFinding::IndexDrift {
1211 kind: IndexKind::ClientOrderId,
1212 drift: IndexDrift::TargetCorrupted { stored_seq: 2 },
1213 ..
1214 }
1215 )),
1216 "findings was: {:?}",
1217 report.findings,
1218 );
1219 }
1220
1221 #[rstest]
1222 fn verify_propagates_no_run_open_as_error() {
1223 let backend = MemoryBackend::new();
1224 let verifier = Verifier::new(Box::new(backend));
1225
1226 let err = verifier.verify().expect_err("must fail");
1227
1228 match err {
1229 VerifyError::Backend(EventStoreError::Backend(msg)) => {
1230 assert!(msg.contains("no run open"), "msg was: {msg}");
1231 }
1232 VerifyError::Backend(other) => {
1233 panic!("expected Backend(no run open), was {other:?}")
1234 }
1235 }
1236 }
1237}