Skip to main content

mnesis_store_testing/
linearizability.rs

1//! Linearizability/Isolation conformance: genuinely-overlapping writers and a
2//! parked subscriber. Real overlap via `tokio::spawn` + `Barrier` (CLAUDE
3//! rule 8 — never sequential-then-check).
4
5use core::future::Future;
6use core::time::Duration;
7use std::sync::Arc;
8
9use futures::pin_mut;
10use mnesis::Version;
11use mnesis_store::store::RawEventStore;
12use mnesis_store::wake::WakeSource;
13use mnesis_store::{AppendError, PendingBatch, Step, StreamKey, Subscription};
14use tokio::sync::Barrier;
15use tokio::time::timeout;
16
17use crate::row::{
18    ConformanceRow, SubId, append_event, append_rows, assert_strictly_increasing, drain_all,
19    drain_stream, envelope_for,
20};
21
22const WAIT: Duration = Duration::from_secs(10);
23const WRITERS: usize = 8;
24
25/// N overlapping appenders race the same fresh stream with the same
26/// expectation: exactly ONE wins, every loser sees Conflict, and the store
27/// holds exactly the winner's event.
28pub async fn check_concurrent_same_stream_single_winner<S, C, F, Fut>(factory: &F)
29where
30    S: RawEventStore + WakeSource,
31    C: Send,
32    F: Fn() -> Fut + Send + Sync,
33    Fut: Future<Output = (S, C)> + Send,
34{
35    let (raw, _guard) = factory().await;
36    let store = Arc::new(raw);
37    let id = StreamKey::from_slice(b"race");
38    let barrier = Arc::new(Barrier::new(WRITERS));
39
40    let mut handles = Vec::with_capacity(WRITERS);
41    for i in 0..WRITERS {
42        let task_store = Arc::clone(&store);
43        let task_barrier = Arc::clone(&barrier);
44        let task_id = id.clone();
45        handles.push(tokio::spawn(async move {
46            let payload = vec![u8::try_from(i).unwrap_or(0)];
47            let env = envelope_for(&ConformanceRow::new(1, "E", payload));
48            task_barrier.wait().await;
49            task_store
50                .append(&task_id, None, PendingBatch::of(&env))
51                .await
52        }));
53    }
54
55    let mut winners = Vec::new();
56    let mut conflicts = 0;
57    for (i, h) in handles.into_iter().enumerate() {
58        match h.await.expect("writer task panicked") {
59            Ok(_position) => winners.push(i),
60            Err(AppendError::Conflict { .. }) => conflicts += 1,
61            Err(other) => panic!("writer {i} hit a non-conflict error: {other:?}"),
62        }
63    }
64    assert_eq!(
65        winners.len(),
66        1,
67        "exactly one concurrent appender must win, got {winners:?}"
68    );
69    assert_eq!(conflicts, WRITERS - 1, "every loser must see Conflict");
70
71    let got = drain_stream(store.as_ref(), &id, Version::INITIAL).await;
72    assert_eq!(got.len(), 1, "store must hold exactly the winner's event");
73    assert_eq!(
74        got[0].payload,
75        vec![u8::try_from(winners[0]).unwrap_or(0)],
76        "the persisted event must be the winner's",
77    );
78}
79
80/// Overlapping appenders on DISTINCT streams never conflict; every event
81/// lands; `$all` holds all of them with strictly increasing positions.
82pub async fn check_concurrent_distinct_streams_all_land<S, C, F, Fut>(factory: &F)
83where
84    S: RawEventStore + WakeSource,
85    C: Send,
86    F: Fn() -> Fut + Send + Sync,
87    Fut: Future<Output = (S, C)> + Send,
88{
89    const PER_STREAM: u64 = 5;
90    let (raw, _guard) = factory().await;
91    let store = Arc::new(raw);
92    let barrier = Arc::new(Barrier::new(WRITERS));
93
94    let mut handles = Vec::with_capacity(WRITERS);
95    for i in 0..WRITERS {
96        let task_store = Arc::clone(&store);
97        let task_barrier = Arc::clone(&barrier);
98        handles.push(tokio::spawn(async move {
99            let id = StreamKey::from_slice(format!("s{i}").as_bytes());
100            task_barrier.wait().await;
101            for v in 1..=PER_STREAM {
102                append_event(task_store.as_ref(), &id, v, format!("s{i}v{v}").as_bytes()).await;
103            }
104        }));
105    }
106    for h in handles {
107        h.await.expect("writer task panicked");
108    }
109
110    let all = drain_all(store.as_ref(), None).await;
111    assert_eq!(
112        all.len(),
113        WRITERS * usize::try_from(PER_STREAM).unwrap_or(usize::MAX),
114        "every concurrently appended event must land in $all",
115    );
116    assert_strictly_increasing(&all);
117
118    for i in 0..WRITERS {
119        let id = StreamKey::from_slice(format!("s{i}").as_bytes());
120        let got = drain_stream(store.as_ref(), &id, Version::INITIAL).await;
121        let want: Vec<Vec<u8>> = (1..=PER_STREAM)
122            .map(|v| format!("s{i}v{v}").into_bytes())
123            .collect();
124        let payloads: Vec<Vec<u8>> = got.iter().map(|r| r.payload.clone()).collect();
125        assert_eq!(
126            payloads, want,
127            "stream s{i} must hold its own events in order"
128        );
129    }
130}
131
132/// Wake-after-idle: a subscriber parked at `CaughtUp` is woken by a later
133/// append from another task — the lost-wakeup race the arm-before-rescan
134/// discipline exists to prevent.
135pub async fn check_wake_after_idle<S, C, F, Fut>(factory: &F)
136where
137    S: RawEventStore + WakeSource,
138    S::Stream: Unpin,
139    C: Send,
140    F: Fn() -> Fut + Send + Sync,
141    Fut: Future<Output = (S, C)> + Send,
142{
143    let (raw, _guard) = factory().await;
144    let store = raw.into_store();
145    let id = SubId::new("wake-idle");
146    append_event(&store, &id.key(), 1, b"p1").await;
147
148    let sub = Subscription::new(&store);
149    let stream = sub
150        .subscribe(&id, None)
151        .unwrap_or_else(|e| panic!("register failed: {e:?}"));
152    pin_mut!(stream);
153
154    // Drain to CaughtUp.
155    loop {
156        match timeout(WAIT, futures::StreamExt::next(&mut stream))
157            .await
158            .expect("backlog must not hang")
159            .expect("subscription must not end")
160            .unwrap_or_else(|e| panic!("item errored: {e:?}"))
161        {
162            Step::CaughtUp => break,
163            Step::Event(_) => {}
164        }
165    }
166
167    // Park, then append from another task after a real delay.
168    let writer_store = store.clone();
169    let key = id.key();
170    let writer = tokio::spawn(async move {
171        tokio::time::sleep(Duration::from_millis(100)).await;
172        append_event(&writer_store, &key, 2, b"p2").await;
173    });
174
175    let woke = timeout(WAIT, futures::StreamExt::next(&mut stream))
176        .await
177        .expect("parked subscriber was never woken — lost wakeup")
178        .expect("subscription must not end")
179        .unwrap_or_else(|e| panic!("item errored: {e:?}"));
180    match woke {
181        Step::Event(env) => assert_eq!(env.version().as_u64(), 2, "wake must deliver v2"),
182        Step::CaughtUp => panic!("CaughtUp must be emitted exactly once"),
183    }
184    writer.await.expect("writer task panicked");
185}
186
187/// Appends racing the catch-up→live boundary are neither lost nor duplicated,
188/// and `CaughtUp` is still emitted exactly once.
189pub async fn check_caught_up_boundary_race<S, C, F, Fut>(factory: &F)
190where
191    S: RawEventStore + WakeSource,
192    S::Stream: Unpin,
193    C: Send,
194    F: Fn() -> Fut + Send + Sync,
195    Fut: Future<Output = (S, C)> + Send,
196{
197    const BACKLOG: u64 = 100;
198    const LIVE: u64 = 100;
199    let (raw, _guard) = factory().await;
200    let store = raw.into_store();
201    let id = SubId::new("boundary-race");
202    let rows: Vec<_> = (1..=BACKLOG)
203        .map(|v| ConformanceRow::new(v, "E", vec![]))
204        .collect();
205    append_rows(&store, &id.key(), &rows).await;
206
207    let sub = Subscription::new(&store);
208    let stream = sub
209        .subscribe(&id, None)
210        .unwrap_or_else(|e| panic!("register failed: {e:?}"));
211    pin_mut!(stream);
212
213    // Writer races the reader's catch-up.
214    let writer_store = store.clone();
215    let key = id.key();
216    let writer = tokio::spawn(async move {
217        for v in (BACKLOG + 1)..=(BACKLOG + LIVE) {
218            append_event(&writer_store, &key, v, b"live").await;
219        }
220    });
221
222    // Loop until BOTH every event has arrived AND `CaughtUp` has been seen —
223    // exiting on event-count alone would miss a `CaughtUp` that lands after
224    // the writer has raced ahead of the reader's catch-up scan and delivered
225    // the entire backlog+live run before the boundary is detected.
226    let total = BACKLOG + LIVE;
227    let want_events = usize::try_from(total).unwrap_or(usize::MAX);
228    let mut versions = Vec::with_capacity(want_events);
229    let mut caught_up = 0u32;
230    while versions.len() < want_events || caught_up == 0 {
231        match timeout(WAIT, futures::StreamExt::next(&mut stream))
232            .await
233            .expect("boundary race hung — event lost across the catch-up→live seam")
234            .expect("subscription must not end")
235            .unwrap_or_else(|e| panic!("item errored: {e:?}"))
236        {
237            Step::Event(env) => versions.push(env.version().as_u64()),
238            Step::CaughtUp => caught_up += 1,
239        }
240    }
241    writer.await.expect("writer task panicked");
242
243    assert_eq!(
244        caught_up, 1,
245        "CaughtUp must be emitted exactly once, got {caught_up}"
246    );
247    let want: Vec<u64> = (1..=total).collect();
248    assert_eq!(
249        versions, want,
250        "all {total} events must arrive exactly once, in order, across the boundary",
251    );
252}