1use indexmap::IndexMap;
20use nautilus_core::UnixNanos;
21
22use crate::{
23 backend::{AppendEntry, EventStore, IndexKey, IndexKind, ScanDirection},
24 entry::EventStoreEntry,
25 error::EventStoreError,
26 manifest::{RunManifest, RunStatus},
27 snapshot::{SnapshotAnchor, validate_new_anchor},
28};
29
30#[derive(Debug, Default)]
43pub struct MemoryBackend {
44 state: Option<RunState>,
45}
46
47#[derive(Debug)]
48struct RunState {
49 manifest: RunManifest,
50 entries: Vec<EventStoreEntry>,
51 indices: Indices,
52 snapshot_anchor: Option<SnapshotAnchor>,
53 high_watermark: u64,
54 max_ts_init: UnixNanos,
55}
56
57#[derive(Debug, Default)]
58struct Indices {
59 client_order: IndexMap<String, u64>,
60 venue_order: IndexMap<String, u64>,
61}
62
63impl Indices {
64 fn map_for(&self, kind: IndexKind) -> &IndexMap<String, u64> {
65 match kind {
66 IndexKind::ClientOrderId => &self.client_order,
67 IndexKind::VenueOrderId => &self.venue_order,
68 }
69 }
70
71 fn map_for_mut(&mut self, kind: IndexKind) -> &mut IndexMap<String, u64> {
72 match kind {
73 IndexKind::ClientOrderId => &mut self.client_order,
74 IndexKind::VenueOrderId => &mut self.venue_order,
75 }
76 }
77}
78
79impl MemoryBackend {
80 #[must_use]
82 pub fn new() -> Self {
83 Self::default()
84 }
85
86 fn state(&self) -> Result<&RunState, EventStoreError> {
87 self.state
88 .as_ref()
89 .ok_or_else(|| EventStoreError::Backend("no run open".to_string()))
90 }
91
92 fn state_mut(&mut self) -> Result<&mut RunState, EventStoreError> {
93 self.state
94 .as_mut()
95 .ok_or_else(|| EventStoreError::Backend("no run open".to_string()))
96 }
97}
98
99impl EventStore for MemoryBackend {
100 fn open_run(&mut self, mut manifest: RunManifest) -> Result<(), EventStoreError> {
101 if let Some(state) = &self.state {
102 if !state.manifest.is_sealed() {
103 return Err(EventStoreError::CrashedPredecessor);
104 }
105
106 if state.manifest.run_id == manifest.run_id {
109 return Err(EventStoreError::Backend(format!(
110 "run {} already sealed, status was {:?}",
111 state.manifest.run_id, state.manifest.status,
112 )));
113 }
114 }
115
116 manifest.status = RunStatus::Running;
117 manifest.end_ts_init = None;
118 manifest.high_watermark = 0;
119
120 self.state = Some(RunState {
121 manifest,
122 entries: Vec::new(),
123 indices: Indices::default(),
124 snapshot_anchor: None,
125 high_watermark: 0,
126 max_ts_init: UnixNanos::default(),
127 });
128 Ok(())
129 }
130
131 fn append_batch(&mut self, entries: &[AppendEntry]) -> Result<u64, EventStoreError> {
132 let state = self.state_mut()?;
133
134 if state.manifest.is_sealed() {
135 return Err(EventStoreError::Closed);
136 }
137
138 if entries.is_empty() {
139 return Ok(state.high_watermark);
140 }
141
142 for (expected, append) in (state.high_watermark + 1..).zip(entries.iter()) {
143 if append.entry.seq != expected {
144 return Err(EventStoreError::OutOfOrder {
148 high_watermark: state.high_watermark,
149 seq: append.entry.seq,
150 });
151 }
152 }
153
154 for append in entries {
155 for IndexKey { kind, key } in &append.index_keys {
156 state
157 .indices
158 .map_for_mut(*kind)
159 .entry(key.clone())
160 .or_insert(append.entry.seq);
161 }
162
163 if append.entry.ts_init > state.max_ts_init {
164 state.max_ts_init = append.entry.ts_init;
165 }
166 state.high_watermark = append.entry.seq;
167 state.entries.push(append.entry.clone());
168 }
169
170 state.manifest.high_watermark = state.high_watermark;
171 Ok(state.high_watermark)
172 }
173
174 fn scan_range(
175 &self,
176 from: u64,
177 to: u64,
178 direction: ScanDirection,
179 ) -> Result<Vec<EventStoreEntry>, EventStoreError> {
180 let state = self.state()?;
181
182 if from > to || from == 0 || state.entries.is_empty() {
183 return Ok(Vec::new());
184 }
185
186 let lo = usize::try_from(from)
187 .unwrap_or(usize::MAX)
188 .saturating_sub(1);
189 let hi = usize::try_from(to)
190 .unwrap_or(usize::MAX)
191 .min(state.entries.len());
192
193 if lo >= hi {
194 return Ok(Vec::new());
195 }
196
197 let slice = &state.entries[lo..hi];
198 for entry in slice {
199 if entry.recompute_hash() != entry.entry_hash {
200 return Err(EventStoreError::HashMismatch { seq: entry.seq });
201 }
202 }
203
204 let mut out: Vec<EventStoreEntry> = slice.to_vec();
205 if matches!(direction, ScanDirection::Reverse) {
206 out.reverse();
207 }
208 Ok(out)
209 }
210
211 fn scan_seq(&self, seq: u64) -> Result<Option<EventStoreEntry>, EventStoreError> {
212 let state = self.state()?;
213
214 if seq == 0 || seq > state.high_watermark {
215 return Ok(None);
216 }
217
218 let idx = usize::try_from(seq - 1)
219 .map_err(|e| EventStoreError::Backend(format!("seq {seq} out of usize range: {e}")))?;
220 let entry = &state.entries[idx];
221 if entry.recompute_hash() != entry.entry_hash {
222 return Err(EventStoreError::HashMismatch { seq });
223 }
224 Ok(Some(entry.clone()))
225 }
226
227 fn lookup(&self, kind: IndexKind, key: &str) -> Result<Option<u64>, EventStoreError> {
228 let state = self.state()?;
229 Ok(state.indices.map_for(kind).get(key).copied())
230 }
231
232 fn iter_index_keys(&self, kind: IndexKind) -> Result<Vec<(String, u64)>, EventStoreError> {
233 let state = self.state()?;
234 Ok(state
235 .indices
236 .map_for(kind)
237 .iter()
238 .map(|(k, v)| (k.clone(), *v))
239 .collect())
240 }
241
242 fn record_snapshot_anchor(&mut self, anchor: SnapshotAnchor) -> Result<(), EventStoreError> {
243 let state = self.state_mut()?;
244
245 if state.manifest.is_sealed() {
246 return Err(EventStoreError::Closed);
247 }
248
249 validate_new_anchor(
250 &anchor,
251 state.high_watermark,
252 state.snapshot_anchor.as_ref(),
253 )?;
254 state.snapshot_anchor = Some(anchor);
255 Ok(())
256 }
257
258 fn latest_snapshot_anchor(&self) -> Result<Option<SnapshotAnchor>, EventStoreError> {
259 Ok(self.state()?.snapshot_anchor.clone())
260 }
261
262 fn seal(&mut self, status: RunStatus) -> Result<(), EventStoreError> {
263 let state = self.state_mut()?;
264
265 if matches!(status, RunStatus::Running) {
269 return Err(EventStoreError::Backend(
270 "seal status must be a terminal state, was Running".to_string(),
271 ));
272 }
273
274 if state.manifest.is_sealed() {
275 return Err(EventStoreError::Closed);
276 }
277
278 state.manifest.status = status;
279 state.manifest.high_watermark = state.high_watermark;
280 if state.high_watermark > 0 {
281 state.manifest.end_ts_init = Some(state.max_ts_init);
282 }
283 Ok(())
284 }
285
286 fn manifest(&self) -> Result<RunManifest, EventStoreError> {
287 Ok(self.state()?.manifest.clone())
288 }
289
290 fn high_watermark(&self) -> Result<u64, EventStoreError> {
291 Ok(self.state()?.high_watermark)
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use bytes::Bytes;
298 use indexmap::IndexMap;
299 use nautilus_core::{UUID4, UnixNanos};
300 use rstest::{fixture, rstest};
301 use ustr::Ustr;
302
303 use super::*;
304 use crate::{
305 compute_entry_hash,
306 entry::{EventStoreEntry, Topic},
307 headers::Headers,
308 manifest::{RegisteredComponents, RunManifest, RunStatus},
309 };
310
311 fn manifest(run_id: &str) -> RunManifest {
312 RunManifest {
313 run_id: run_id.to_string(),
314 parent_run_id: None,
315 instance_id: "trader-001".to_string(),
316 binary_hash: "deadbeef".to_string(),
317 schema_version: 1,
318 crate_versions: "feedface".to_string(),
319 feature_flags: Vec::new(),
320 adapter_versions: IndexMap::new(),
321 config_hash: "cafebabe".to_string(),
322 registered_components: RegisteredComponents::default(),
323 seed: None,
324 start_ts_init: UnixNanos::from(0),
325 end_ts_init: None,
326 high_watermark: 0,
327 status: RunStatus::Running,
328 }
329 }
330
331 fn build_entry(seq: u64, headers: Headers, ts_init: u64) -> EventStoreEntry {
332 let topic: Topic = "exec.command.SubmitOrder".into();
333 let payload_type = Ustr::from("SubmitOrder");
334 let payload = Bytes::from_static(b"\x01\x02\x03\x04");
335 let ts_publish = UnixNanos::from(ts_init + 1);
336 let ts_init = UnixNanos::from(ts_init);
337 let hash = compute_entry_hash(
338 seq,
339 ts_init,
340 ts_publish,
341 topic.as_ref(),
342 payload_type.as_str(),
343 &payload,
344 &headers,
345 );
346
347 EventStoreEntry::new(
348 hash,
349 seq,
350 headers,
351 topic,
352 payload_type,
353 payload,
354 ts_init,
355 ts_publish,
356 )
357 }
358
359 fn append_with(seq: u64, ts_init: u64, index_keys: Vec<IndexKey>) -> AppendEntry {
360 AppendEntry::new(build_entry(seq, Headers::empty(), ts_init), index_keys)
361 }
362
363 #[fixture]
364 fn open_backend() -> MemoryBackend {
365 let mut backend = MemoryBackend::new();
366 backend
367 .open_run(manifest("1700000000-aaaa1111"))
368 .expect("open run");
369 backend
370 }
371
372 #[rstest]
373 fn manifest_errors_when_no_run_open() {
374 let backend = MemoryBackend::new();
375
376 match backend.manifest() {
377 Err(EventStoreError::Backend(msg)) => {
378 assert!(msg.contains("no run open"), "msg was: {msg}");
379 }
380 other => panic!("expected Backend, was {other:?}"),
381 }
382
383 match backend.high_watermark() {
384 Err(EventStoreError::Backend(msg)) => {
385 assert!(msg.contains("no run open"), "msg was: {msg}");
386 }
387 other => panic!("expected Backend, was {other:?}"),
388 }
389 }
390
391 #[rstest]
392 #[case::append_batch("append_batch")]
393 #[case::scan_range("scan_range")]
394 #[case::scan_seq("scan_seq")]
395 #[case::lookup("lookup")]
396 #[case::record_snapshot_anchor("record_snapshot_anchor")]
397 #[case::latest_snapshot_anchor("latest_snapshot_anchor")]
398 #[case::seal("seal")]
399 fn methods_error_when_no_run_open(#[case] op: &str) {
400 let mut backend = MemoryBackend::new();
401 let err = match op {
402 "append_batch" => backend.append_batch(&[]).unwrap_err(),
403 "scan_range" => backend
404 .scan_range(1, 1, ScanDirection::Forward)
405 .unwrap_err(),
406 "scan_seq" => backend.scan_seq(1).unwrap_err(),
407 "lookup" => backend.lookup(IndexKind::ClientOrderId, "k").unwrap_err(),
408 "record_snapshot_anchor" => backend
409 .record_snapshot_anchor(SnapshotAnchor::new(0, "blob", "hash"))
410 .unwrap_err(),
411 "latest_snapshot_anchor" => backend.latest_snapshot_anchor().unwrap_err(),
412 "seal" => backend.seal(RunStatus::Ended).unwrap_err(),
413 _ => unreachable!(),
414 };
415
416 match err {
417 EventStoreError::Backend(msg) => {
418 assert!(msg.contains("no run open"), "msg was: {msg}");
419 }
420 other => panic!("expected Backend, was {other:?}"),
421 }
422 }
423
424 #[rstest]
425 fn open_run_normalizes_status_and_zeroes_progress(open_backend: MemoryBackend) {
426 let m = open_backend.manifest().expect("manifest");
427
428 assert_eq!(m.status, RunStatus::Running);
429 assert_eq!(m.high_watermark, 0);
430 assert!(m.end_ts_init.is_none());
431 assert_eq!(open_backend.high_watermark().expect("hwm"), 0);
432 }
433
434 #[rstest]
435 fn append_advances_high_watermark(mut open_backend: MemoryBackend) {
436 let batch = vec![
437 append_with(1, 10, Vec::new()),
438 append_with(2, 11, Vec::new()),
439 append_with(3, 12, Vec::new()),
440 ];
441
442 let hwm = open_backend.append_batch(&batch).expect("append");
443
444 assert_eq!(hwm, 3);
445 assert_eq!(open_backend.high_watermark().expect("hwm"), 3);
446 assert_eq!(open_backend.manifest().expect("m").high_watermark, 3);
447 }
448
449 #[rstest]
450 fn append_rejects_first_seq_not_at_watermark_plus_one(mut open_backend: MemoryBackend) {
451 let batch = vec![append_with(2, 10, Vec::new())];
452
453 let err = open_backend.append_batch(&batch).expect_err("must reject");
454
455 assert!(matches!(
456 err,
457 EventStoreError::OutOfOrder {
458 high_watermark: 0,
459 seq: 2,
460 }
461 ));
462 }
463
464 #[rstest]
465 fn append_rejects_within_batch_seq_gap(mut open_backend: MemoryBackend) {
466 let batch = vec![
467 append_with(1, 10, Vec::new()),
468 append_with(3, 11, Vec::new()),
469 ];
470
471 let err = open_backend.append_batch(&batch).expect_err("must reject");
472
473 assert!(matches!(
475 err,
476 EventStoreError::OutOfOrder {
477 high_watermark: 0,
478 seq: 3,
479 }
480 ));
481 assert_eq!(open_backend.high_watermark().expect("hwm"), 0);
483 }
484
485 #[rstest]
486 fn append_after_seal_returns_closed(mut open_backend: MemoryBackend) {
487 open_backend
488 .append_batch(&[append_with(1, 10, Vec::new())])
489 .expect("append");
490 open_backend.seal(RunStatus::Ended).expect("seal");
491
492 let err = open_backend
493 .append_batch(&[append_with(2, 11, Vec::new())])
494 .expect_err("must reject");
495
496 assert!(matches!(err, EventStoreError::Closed));
497 }
498
499 #[rstest]
500 fn empty_batch_is_a_noop(mut open_backend: MemoryBackend) {
501 let hwm = open_backend.append_batch(&[]).expect("append");
502
503 assert_eq!(hwm, 0);
504 assert_eq!(open_backend.high_watermark().expect("hwm"), 0);
505 }
506
507 #[rstest]
508 fn snapshot_anchor_is_none_until_recorded(open_backend: MemoryBackend) {
509 assert!(
510 open_backend
511 .latest_snapshot_anchor()
512 .expect("latest anchor")
513 .is_none()
514 );
515 }
516
517 #[rstest]
518 fn snapshot_anchor_round_trips(mut open_backend: MemoryBackend) {
519 open_backend
520 .append_batch(&[append_with(1, 10, Vec::new())])
521 .expect("append");
522 let anchor = SnapshotAnchor::new(1, "cache://snapshots/run-1/1", "blake3:abc");
523
524 open_backend
525 .record_snapshot_anchor(anchor.clone())
526 .expect("record anchor");
527
528 assert_eq!(
529 open_backend
530 .latest_snapshot_anchor()
531 .expect("latest anchor"),
532 Some(anchor),
533 );
534 }
535
536 #[rstest]
537 fn snapshot_anchor_rejects_watermark_past_durable_hwm(mut open_backend: MemoryBackend) {
538 let anchor = SnapshotAnchor::new(1, "cache://snapshots/run-1/1", "blake3:abc");
539 let err = open_backend
540 .record_snapshot_anchor(anchor)
541 .expect_err("must reject");
542
543 match err {
544 EventStoreError::Backend(msg) => {
545 assert!(
546 msg.contains("exceeds durable high_watermark"),
547 "msg was: {msg}",
548 );
549 }
550 other => panic!("expected Backend, was {other:?}"),
551 }
552 }
553
554 #[rstest]
555 fn snapshot_anchor_rejects_backward_move(mut open_backend: MemoryBackend) {
556 open_backend
557 .append_batch(&[
558 append_with(1, 10, Vec::new()),
559 append_with(2, 11, Vec::new()),
560 ])
561 .expect("append");
562 open_backend
563 .record_snapshot_anchor(SnapshotAnchor::new(2, "latest", "hash-latest"))
564 .expect("record latest");
565
566 let err = open_backend
567 .record_snapshot_anchor(SnapshotAnchor::new(1, "older", "hash-older"))
568 .expect_err("must reject older anchor");
569
570 match err {
571 EventStoreError::Backend(msg) => {
572 assert!(msg.contains("older than latest anchor"), "msg was: {msg}");
573 }
574 other => panic!("expected Backend, was {other:?}"),
575 }
576 }
577
578 #[rstest]
579 fn snapshot_anchor_after_seal_returns_closed(mut open_backend: MemoryBackend) {
580 open_backend
581 .append_batch(&[append_with(1, 10, Vec::new())])
582 .expect("append");
583 open_backend.seal(RunStatus::Ended).expect("seal");
584
585 let err = open_backend
586 .record_snapshot_anchor(SnapshotAnchor::new(1, "blob", "hash"))
587 .expect_err("must reject");
588
589 assert!(matches!(err, EventStoreError::Closed));
590 }
591
592 #[rstest]
593 fn scan_seq_returns_committed_entry(mut open_backend: MemoryBackend) {
594 open_backend
595 .append_batch(&[
596 append_with(1, 10, Vec::new()),
597 append_with(2, 11, Vec::new()),
598 ])
599 .expect("append");
600
601 let entry = open_backend.scan_seq(2).expect("scan").expect("present");
602
603 assert_eq!(entry.seq, 2);
604 assert_eq!(entry.ts_init, UnixNanos::from(11));
605 }
606
607 #[rstest]
608 fn scan_seq_returns_none_outside_watermark(mut open_backend: MemoryBackend) {
609 open_backend
610 .append_batch(&[append_with(1, 10, Vec::new())])
611 .expect("append");
612
613 assert!(open_backend.scan_seq(0).expect("scan").is_none());
614 assert!(open_backend.scan_seq(2).expect("scan").is_none());
615 }
616
617 #[rstest]
618 #[case::forward_full(1, 3, ScanDirection::Forward, vec![1, 2, 3])]
619 #[case::reverse_full(1, 3, ScanDirection::Reverse, vec![3, 2, 1])]
620 #[case::forward_window(2, 3, ScanDirection::Forward, vec![2, 3])]
621 #[case::reverse_window(2, 3, ScanDirection::Reverse, vec![3, 2])]
622 #[case::clipped_to_watermark(2, 99, ScanDirection::Forward, vec![2, 3])]
623 #[case::reverse_clipped(2, 99, ScanDirection::Reverse, vec![3, 2])]
624 #[case::empty_inverted(3, 1, ScanDirection::Forward, vec![])]
625 #[case::empty_zero(0, 0, ScanDirection::Forward, vec![])]
626 fn scan_range_yields_expected_seqs(
627 mut open_backend: MemoryBackend,
628 #[case] from: u64,
629 #[case] to: u64,
630 #[case] direction: ScanDirection,
631 #[case] expected: Vec<u64>,
632 ) {
633 open_backend
634 .append_batch(&[
635 append_with(1, 10, Vec::new()),
636 append_with(2, 11, Vec::new()),
637 append_with(3, 12, Vec::new()),
638 ])
639 .expect("append");
640
641 let seqs: Vec<u64> = open_backend
642 .scan_range(from, to, direction)
643 .expect("scan")
644 .into_iter()
645 .map(|e| e.seq)
646 .collect();
647
648 assert_eq!(seqs, expected);
649 }
650
651 #[rstest]
652 fn lookup_records_first_occurrence_per_kind(mut open_backend: MemoryBackend) {
653 let cl_ord = "O-1".to_string();
654 let venue = "V-1".to_string();
655 open_backend
656 .append_batch(&[
657 AppendEntry::new(
658 build_entry(1, Headers::empty(), 10),
659 vec![
660 IndexKey::new(IndexKind::ClientOrderId, cl_ord.clone()),
661 IndexKey::new(IndexKind::VenueOrderId, venue.clone()),
662 ],
663 ),
664 AppendEntry::new(
665 build_entry(2, Headers::empty(), 11),
666 vec![
667 IndexKey::new(IndexKind::ClientOrderId, cl_ord.clone()),
669 IndexKey::new(IndexKind::VenueOrderId, venue.clone()),
670 ],
671 ),
672 ])
673 .expect("append");
674
675 assert_eq!(
676 open_backend
677 .lookup(IndexKind::ClientOrderId, &cl_ord)
678 .expect("lookup"),
679 Some(1),
680 );
681 assert_eq!(
682 open_backend
683 .lookup(IndexKind::VenueOrderId, &venue)
684 .expect("lookup"),
685 Some(1),
686 );
687 assert!(
688 open_backend
689 .lookup(IndexKind::ClientOrderId, "missing")
690 .expect("lookup")
691 .is_none(),
692 );
693 }
694
695 #[rstest]
696 fn within_entry_duplicate_keys_resolve_to_first_seq(mut open_backend: MemoryBackend) {
697 let key = "O-1".to_string();
701 open_backend
702 .append_batch(&[
703 AppendEntry::new(
704 build_entry(1, Headers::empty(), 10),
705 vec![
706 IndexKey::new(IndexKind::ClientOrderId, key.clone()),
707 IndexKey::new(IndexKind::ClientOrderId, key.clone()),
708 ],
709 ),
710 AppendEntry::new(
711 build_entry(2, Headers::empty(), 11),
712 vec![IndexKey::new(IndexKind::ClientOrderId, key.clone())],
713 ),
714 ])
715 .expect("append");
716
717 assert_eq!(
718 open_backend
719 .lookup(IndexKind::ClientOrderId, &key)
720 .expect("lookup"),
721 Some(1),
722 );
723 }
724
725 #[rstest]
726 fn lookup_isolates_keys_by_kind(mut open_backend: MemoryBackend) {
727 let key = "shared".to_string();
729 open_backend
730 .append_batch(&[AppendEntry::new(
731 build_entry(1, Headers::empty(), 10),
732 vec![IndexKey::new(IndexKind::ClientOrderId, key.clone())],
733 )])
734 .expect("append");
735
736 assert_eq!(
737 open_backend
738 .lookup(IndexKind::ClientOrderId, &key)
739 .expect("lookup"),
740 Some(1),
741 );
742 assert!(
743 open_backend
744 .lookup(IndexKind::VenueOrderId, &key)
745 .expect("lookup")
746 .is_none(),
747 );
748 }
749
750 #[rstest]
751 #[case::ended(RunStatus::Ended)]
752 #[case::crashed_recovered(RunStatus::CrashedRecovered)]
753 #[case::quarantined(RunStatus::Quarantined)]
754 fn seal_stamps_end_ts_and_blocks_re_seal(
755 mut open_backend: MemoryBackend,
756 #[case] status: RunStatus,
757 ) {
758 open_backend
759 .append_batch(&[
760 append_with(1, 10, Vec::new()),
761 append_with(2, 25, Vec::new()),
762 append_with(3, 17, Vec::new()),
763 ])
764 .expect("append");
765
766 open_backend.seal(status).expect("seal");
767
768 let m = open_backend.manifest().expect("manifest");
769 assert_eq!(m.status, status);
770 assert_eq!(m.high_watermark, 3);
771 assert_eq!(m.end_ts_init, Some(UnixNanos::from(25)));
773
774 let err = open_backend.seal(RunStatus::Ended).expect_err("re-seal");
775 assert!(matches!(err, EventStoreError::Closed));
776 }
777
778 #[rstest]
779 fn seal_rejects_running_status(mut open_backend: MemoryBackend) {
780 let err = open_backend
783 .seal(RunStatus::Running)
784 .expect_err("must reject");
785
786 match err {
787 EventStoreError::Backend(msg) => {
788 assert!(msg.contains("Running"), "msg was: {msg}");
789 }
790 other => panic!("expected Backend, was {other:?}"),
791 }
792 assert!(!open_backend.manifest().expect("manifest").is_sealed());
793 open_backend
795 .append_batch(&[append_with(1, 10, Vec::new())])
796 .expect("append");
797 }
798
799 #[rstest]
800 fn seal_with_no_entries_leaves_end_ts_unset(mut open_backend: MemoryBackend) {
801 open_backend.seal(RunStatus::Ended).expect("seal");
802
803 let m = open_backend.manifest().expect("manifest");
804 assert_eq!(m.status, RunStatus::Ended);
805 assert!(m.end_ts_init.is_none());
806 assert_eq!(m.high_watermark, 0);
807 }
808
809 #[rstest]
810 fn reopening_running_run_returns_crashed_predecessor() {
811 let mut backend = MemoryBackend::new();
812 backend.open_run(manifest("run-1")).expect("open 1");
813 backend
814 .append_batch(&[append_with(1, 10, Vec::new())])
815 .expect("append");
816
817 let err = backend.open_run(manifest("run-2")).expect_err("must flag");
819 assert!(matches!(err, EventStoreError::CrashedPredecessor));
820
821 assert!(
824 backend.scan_seq(1).expect("scan").is_some(),
825 "predecessor entry must survive failed open_run",
826 );
827
828 backend.seal(RunStatus::CrashedRecovered).expect("seal");
830 backend.open_run(manifest("run-2")).expect("open 2");
831 assert_eq!(
832 backend.manifest().expect("manifest").run_id,
833 "run-2".to_string(),
834 );
835 assert_eq!(backend.high_watermark().expect("hwm"), 0);
836 }
837
838 #[rstest]
839 fn reopening_after_clean_seal_succeeds() {
840 let mut backend = MemoryBackend::new();
841 backend.open_run(manifest("run-1")).expect("open 1");
842 backend.seal(RunStatus::Ended).expect("seal");
843
844 backend.open_run(manifest("run-2")).expect("open 2");
845 assert_eq!(
846 backend.manifest().expect("manifest").run_id,
847 "run-2".to_string(),
848 );
849 }
850
851 #[rstest]
852 fn reopening_same_run_id_after_seal_fails() {
853 let mut backend = MemoryBackend::new();
856 backend.open_run(manifest("run-1")).expect("open 1");
857 backend
858 .append_batch(&[append_with(1, 10, Vec::new())])
859 .expect("append");
860 backend.seal(RunStatus::Ended).expect("seal");
861
862 let err = backend
863 .open_run(manifest("run-1"))
864 .expect_err("must refuse same-id sealed reopen");
865
866 match err {
867 EventStoreError::Backend(msg) => {
868 assert!(msg.contains("already sealed"), "msg was: {msg}");
869 }
870 other => panic!("expected Backend, was {other:?}"),
871 }
872
873 assert!(
874 backend.scan_seq(1).expect("scan").is_some(),
875 "predecessor entry must survive refused reopen",
876 );
877 assert_eq!(backend.high_watermark().expect("hwm"), 1);
878 }
879
880 #[rstest]
881 fn scan_recomputes_hash_and_quarantines_on_mismatch(mut open_backend: MemoryBackend) {
882 let mut tampered = build_entry(1, Headers::empty(), 10);
885 tampered.payload = Bytes::from_static(b"\xFF\xFF");
886 open_backend
887 .append_batch(&[AppendEntry::without_indices(tampered)])
888 .expect("append");
889
890 assert!(matches!(
891 open_backend.scan_seq(1),
892 Err(EventStoreError::HashMismatch { seq: 1 }),
893 ));
894 assert!(matches!(
895 open_backend.scan_range(1, 1, ScanDirection::Forward),
896 Err(EventStoreError::HashMismatch { seq: 1 }),
897 ));
898 }
899
900 #[rstest]
901 fn append_extracts_no_indices_when_keys_empty(mut open_backend: MemoryBackend) {
902 let headers = Headers {
905 correlation_id: Some(UUID4::new()),
906 ..Headers::empty()
907 };
908 open_backend
909 .append_batch(&[AppendEntry::without_indices(build_entry(1, headers, 10))])
910 .expect("append");
911
912 assert!(
913 open_backend
914 .lookup(IndexKind::ClientOrderId, "any")
915 .expect("lookup")
916 .is_none(),
917 );
918 }
919
920 #[rstest]
921 fn iter_index_keys_enumerates_first_write_wins_pairs(mut open_backend: MemoryBackend) {
922 open_backend
927 .append_batch(&[
928 AppendEntry::new(
929 build_entry(1, Headers::empty(), 10),
930 vec![
931 IndexKey::new(IndexKind::ClientOrderId, "O-1".to_string()),
932 IndexKey::new(IndexKind::VenueOrderId, "V-1".to_string()),
933 ],
934 ),
935 AppendEntry::new(
936 build_entry(2, Headers::empty(), 11),
937 vec![
938 IndexKey::new(IndexKind::ClientOrderId, "O-1".to_string()),
940 IndexKey::new(IndexKind::ClientOrderId, "O-2".to_string()),
941 ],
942 ),
943 ])
944 .expect("append");
945
946 let mut client = open_backend
947 .iter_index_keys(IndexKind::ClientOrderId)
948 .expect("iter");
949 client.sort();
950 assert_eq!(
951 client,
952 vec![("O-1".to_string(), 1u64), ("O-2".to_string(), 2u64)],
953 );
954
955 let venue = open_backend
956 .iter_index_keys(IndexKind::VenueOrderId)
957 .expect("iter");
958 assert_eq!(venue, vec![("V-1".to_string(), 1u64)]);
959 }
960
961 #[rstest]
962 fn iter_index_keys_errors_when_no_run_open() {
963 let backend = MemoryBackend::new();
964
965 match backend.iter_index_keys(IndexKind::ClientOrderId) {
966 Err(EventStoreError::Backend(msg)) => {
967 assert!(msg.contains("no run open"), "msg was: {msg}");
968 }
969 other => panic!("expected Backend, was {other:?}"),
970 }
971 }
972}