Skip to main content

krabka_remote_storage_topic/log/
in_process.rs

1//! [`InProcessMetadataEventLog`], the in-memory broadcast-channel fixture that
2//! implements [`MetadataEventLog`] without a Kafka cluster behind it.
3//!
4//! This module holds the fixture's shared state, which is the per-partition
5//! record vectors and the per-subscription cursors, together with the
6//! [`MetadataEventLog`] implementation over them. The two child modules act on
7//! that state: [`self::handle`] carries the [`AssignmentHandle`] the fixture
8//! hands back, and [`self::live`] carries the broadcast filter that decides
9//! which live record a subscription sees. They are descendants of this module
10//! so the state can stay private to this file.
11
12use {
13    self::{handle::InProcessAssignmentHandle, live::filtered_broadcast},
14    super::{
15        AssignmentHandle, MetadataEventLog, MetadataEventRecord, MetadataEventStream,
16        PartitionStart, RangeVisitor,
17    },
18    crate::error::MetadataLogError,
19    async_trait::async_trait,
20    bytes::Bytes,
21    futures_util::{
22        StreamExt,
23        stream::{self, unfold},
24    },
25    std::{
26        collections::HashMap,
27        sync::{Arc, Mutex, MutexGuard, PoisonError, atomic::AtomicU64},
28    },
29    tokio::sync::{broadcast, mpsc},
30};
31
32mod handle;
33mod live;
34
35/// Single-process [`MetadataEventLog`] for unit tests and for the
36/// multi-broker fixture. Multiple manager instances that clone the same `Arc`
37/// observe each other's writes.
38pub struct InProcessMetadataEventLog {
39    inner: Arc<InProcessInner>,
40}
41
42/// Live-assignment cursor for one partition within a subscription.
43#[derive(Debug, Clone, Copy)]
44struct PartitionCursor {
45    /// Next offset that the backlog or live path has NOT yet delivered. The
46    /// log filters out records below this offset.
47    next: i64,
48    /// When set, the log forwards live records for this partition through
49    /// the `inject` FIFO rather than emits them directly on the broadcast
50    /// path. A partition added mid-stream sets this, so its live appends
51    /// queue *behind* its already-injected backlog. Without it,
52    /// `stream::select` could interleave a live record ahead of undrained
53    /// backlog and violate per-partition publish order. Initially-assigned
54    /// partitions leave this `false`. Their backlog goes through the chained
55    /// snapshot stream, which fully drains before any live record.
56    via_inject: bool,
57}
58
59/// Per-subscription live assignment, plus a sender to inject backlog when a
60/// partition is added mid-stream. A monotonically-increasing subscription id
61/// keys it, so multiple subscribers stay independent.
62struct SubscriptionState {
63    /// partition -> cursor. Presence in the map means the partition is
64    /// assigned.
65    assigned: Mutex<HashMap<i32, PartitionCursor>>,
66    /// Inject backlog records in FIFO publish order. For `add`-ed partitions
67    /// this also injects live records.
68    inject: mpsc::UnboundedSender<MetadataEventRecord>,
69}
70
71struct InProcessInner {
72    /// `log[partition][offset] = event`.
73    log: Mutex<Vec<Vec<MetadataEventRecord>>>,
74    /// Notify subscribers of new writes.
75    tx: broadcast::Sender<MetadataEventRecord>,
76    /// Constant for the life of the log.
77    partition_count: i32,
78    /// Live subscriptions, keyed by id, for assignment filtering and
79    /// mid-stream backlog injection.
80    subscriptions: Mutex<HashMap<u64, Arc<SubscriptionState>>>,
81    /// Allocates subscription ids.
82    next_sub_id: AtomicU64,
83}
84
85impl InProcessMetadataEventLog {
86    /// Construct an empty log with `partition_count` partitions.
87    ///
88    /// # Panics
89    ///
90    /// Panics when `partition_count <= 0`.
91    #[must_use]
92    pub fn new(partition_count: i32) -> Arc<Self> {
93        assert2::assert!(
94            let Some(cap) = usize::try_from(partition_count).ok().filter(|&cap| cap > 0),
95            "partition_count must be positive"
96        );
97        let (tx, _rx) = broadcast::channel(1024);
98        Arc::new(Self {
99            inner: Arc::new(InProcessInner {
100                log: Mutex::new(vec![Vec::new(); cap]),
101                tx,
102                partition_count,
103                subscriptions: Mutex::new(HashMap::new()),
104                next_sub_id: AtomicU64::new(0),
105            }),
106        })
107    }
108
109    /// The records of every partition, whether or not a thread panicked
110    /// while it held the lock.
111    fn log(&self) -> MutexGuard<'_, Vec<Vec<MetadataEventRecord>>> {
112        self.inner
113            .log
114            .lock()
115            .unwrap_or_else(PoisonError::into_inner)
116    }
117
118    fn append(
119        &self,
120        partition: i32,
121        key: Option<Bytes>,
122        event: Option<Bytes>,
123    ) -> Result<i64, MetadataLogError> {
124        let out_of_range = || MetadataLogError::PartitionOutOfRange {
125            partition,
126            count: self.inner.partition_count,
127        };
128        if partition < 0 || partition >= self.inner.partition_count {
129            return Err(out_of_range());
130        }
131        let idx = usize::try_from(partition).map_err(|_| out_of_range())?;
132        // Hold the partition lock across the broadcast.send so that any
133        // concurrent subscribe() observes either the appended record in
134        // its snapshot or as a forwarded broadcast — never both.
135        let mut guard = self.log();
136        let log_for_p = guard.get_mut(idx).ok_or_else(out_of_range)?;
137        let offset = i64::try_from(log_for_p.len()).map_err(|_| {
138            MetadataLogError::Other(format!(
139                "partition {partition} has more than i64::MAX records"
140            ))
141        })?;
142        let tombstone = event.is_none();
143        let record = MetadataEventRecord {
144            partition,
145            offset,
146            key,
147            payload: event.unwrap_or_default(),
148            tombstone,
149        };
150        log_for_p.push(record.clone());
151        // `send` only errors when there are no active receivers; that
152        // is fine — the record is still durable in the in-memory log
153        // and any later subscriber's snapshot will see it.
154        let _ = self.inner.tx.send(record);
155        drop(guard);
156        Ok(offset)
157    }
158}
159
160#[async_trait]
161impl MetadataEventLog for InProcessMetadataEventLog {
162    fn partition_count(&self) -> i32 { self.inner.partition_count }
163
164    async fn publish(&self, partition: i32, event: Bytes) -> Result<i64, MetadataLogError> {
165        self.append(partition, None, Some(event))
166    }
167
168    async fn publish_keyed(
169        &self,
170        partition: i32,
171        key: Bytes,
172        event: Option<Bytes>,
173    ) -> Result<i64, MetadataLogError> {
174        self.append(partition, Some(key), event)
175    }
176
177    fn subscribe(
178        &self,
179        assignment: Vec<PartitionStart>,
180    ) -> (MetadataEventStream, Arc<dyn AssignmentHandle>) {
181        use std::sync::atomic::Ordering;
182
183        // Bracket snapshot + broadcast subscribe under the log lock so each
184        // published record is seen exactly once (snapshot xor live).
185        let guard = self.log();
186        let rx = self.inner.tx.subscribe();
187
188        // Initial assigned set: partition -> next live offset (= current
189        // len), so the broadcast path forwards only records published after
190        // subscribe; everything earlier comes from the snapshot below.
191        let mut assigned: HashMap<i32, PartitionCursor> = HashMap::new();
192        let mut snapshot: Vec<MetadataEventRecord> = Vec::new();
193        for ps in &assignment {
194            let Ok(idx) = usize::try_from(ps.partition) else {
195                continue;
196            };
197            let Some(records) = guard.get(idx) else {
198                continue;
199            };
200            let begin = usize::try_from(ps.start_offset.max(0)).unwrap_or(usize::MAX);
201            for record in records.iter().skip(begin) {
202                snapshot.push(record.clone());
203            }
204            assigned.insert(ps.partition, PartitionCursor {
205                next: i64::try_from(records.len()).unwrap_or(i64::MAX),
206                // Initially-assigned: backlog rides the chained
207                // snapshot stream, so live records can go direct.
208                via_inject: false,
209            });
210        }
211
212        let (inject_tx, inject_rx) = mpsc::unbounded_channel::<MetadataEventRecord>();
213        let state = Arc::new(SubscriptionState {
214            assigned: Mutex::new(assigned),
215            inject: inject_tx,
216        });
217        let sub_id = self.inner.next_sub_id.fetch_add(1, Ordering::Relaxed);
218        self.inner
219            .subscriptions
220            .lock()
221            .unwrap_or_else(PoisonError::into_inner)
222            .insert(sub_id, Arc::clone(&state));
223        drop(guard);
224
225        let snapshot_stream = stream::iter(snapshot);
226        let inject_stream = unfold(inject_rx, |mut rx| async move {
227            rx.recv().await.map(|r| (r, rx))
228        });
229        let live = filtered_broadcast(rx, Arc::clone(&state), Arc::clone(&self.inner));
230        // Snapshot first (subscribe-time backlog), then a merge of injected
231        // backlog (from `add`) and assignment-filtered live records.
232        let merged = stream::select(inject_stream, live);
233        let stream = snapshot_stream.chain(merged).boxed();
234
235        let handle: Arc<dyn AssignmentHandle> = Arc::new(InProcessAssignmentHandle {
236            inner: Arc::clone(&self.inner),
237            sub_id,
238        });
239        (stream, handle)
240    }
241
242    async fn high_water_marks(&self) -> Result<Vec<i64>, MetadataLogError> {
243        self.log()
244            .iter()
245            .map(|v| {
246                i64::try_from(v.len()).map_err(|_| {
247                    MetadataLogError::Other("a partition has more than i64::MAX records".into())
248                })
249            })
250            .collect()
251    }
252
253    async fn visit_range(
254        &self,
255        partition: i32,
256        start: i64,
257        end: i64,
258        visit: &mut RangeVisitor<'_>,
259    ) -> Result<(), MetadataLogError> {
260        let out_of_range = || MetadataLogError::PartitionOutOfRange {
261            partition,
262            count: self.inner.partition_count,
263        };
264        let idx = usize::try_from(partition).map_err(|_| out_of_range())?;
265        // Copy the range out, so `visit` never runs under the log lock.
266        let copied = self.log().get(idx).map(|records| {
267            let range: Vec<MetadataEventRecord> = records
268                .iter()
269                .filter(|record| start <= record.offset && record.offset < end)
270                .cloned()
271                .collect();
272            (records.len(), range)
273        });
274        let (len, range) = copied.ok_or_else(out_of_range)?;
275        let high_water_mark = i64::try_from(len).map_err(|_| {
276            MetadataLogError::Other(format!(
277                "partition {partition} has more than i64::MAX records"
278            ))
279        })?;
280        if end > high_water_mark {
281            return Err(MetadataLogError::Other(format!(
282                "partition {partition} ends at {high_water_mark}, before {end}"
283            )));
284        }
285        range.into_iter().try_for_each(visit)
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use {
292        super::*,
293        assert2::{assert, check},
294        futures_util::StreamExt,
295        tokio::time,
296    };
297
298    #[tokio::test]
299    async fn publish_assigns_monotonic_offsets() {
300        let log = InProcessMetadataEventLog::new(2);
301        check!(log.publish(0, Bytes::from_static(b"a")).await.unwrap() == 0);
302        check!(log.publish(0, Bytes::from_static(b"b")).await.unwrap() == 1);
303        check!(log.publish(1, Bytes::from_static(b"c")).await.unwrap() == 0);
304        let hwms = log.high_water_marks().await.unwrap();
305        assert!(hwms == vec![2, 1]);
306    }
307
308    /// Every record `visit_range` hands over, in the order it hands them.
309    async fn visited(
310        log: &InProcessMetadataEventLog,
311        partition: i32,
312        start: i64,
313        end: i64,
314    ) -> Result<Vec<MetadataEventRecord>, MetadataLogError> {
315        let mut records = Vec::new();
316        log.visit_range(partition, start, end, &mut |record| {
317            records.push(record);
318            Ok(())
319        })
320        .await?;
321        Ok(records)
322    }
323
324    #[tokio::test]
325    async fn visit_range_hands_over_the_half_open_range_without_subscribing() {
326        let log = InProcessMetadataEventLog::new(2);
327        for value in [b"a", b"b", b"c"] {
328            log.publish(0, Bytes::from_static(value)).await.unwrap();
329        }
330
331        let record = |offset, payload| MetadataEventRecord {
332            partition: 0,
333            offset,
334            key: None,
335            payload: Bytes::from_static(payload),
336            tombstone: false,
337        };
338        check!(visited(&log, 0, 1, 3).await.unwrap() == vec![record(1, b"b"), record(2, b"c")]);
339        check!(visited(&log, 1, 0, 0).await.unwrap().is_empty());
340        check!(
341            visited(&log, 0, 0, 4).await.unwrap_err().to_string()
342                == "metadata log error: partition 0 ends at 3, before 4"
343        );
344        check!(log.inner.subscriptions.lock().unwrap().is_empty());
345    }
346
347    #[yare::parameterized(
348        negative = { -1 },
349        upper_bound = { 2 },
350    )]
351    #[test_macro(tokio::test)]
352    async fn visit_range_rejects_out_of_range_partitions(partition: i32) {
353        let log = InProcessMetadataEventLog::new(2);
354
355        check!(matches!(
356            visited(&log, partition, 0, 0).await,
357            Err(MetadataLogError::PartitionOutOfRange { partition: got, count: 2 })
358                if got == partition
359        ));
360        check!(log.inner.subscriptions.lock().unwrap().is_empty());
361    }
362
363    #[tokio::test]
364    async fn visit_range_stops_at_the_first_visitor_error() {
365        let log = InProcessMetadataEventLog::new(1);
366        for value in [b"a", b"b", b"c"] {
367            log.publish(0, Bytes::from_static(value)).await.unwrap();
368        }
369
370        let mut seen = Vec::new();
371        let error = log
372            .visit_range(0, 0, 3, &mut |record| {
373                seen.push(record.offset);
374                if record.offset == 1 {
375                    Err(MetadataLogError::Closed)
376                } else {
377                    Ok(())
378                }
379            })
380            .await
381            .unwrap_err();
382
383        check!(matches!(error, MetadataLogError::Closed));
384        check!(seen == vec![0, 1]);
385    }
386
387    #[tokio::test]
388    async fn keyed_tombstones_survive_replay() {
389        let log = InProcessMetadataEventLog::new(1);
390        log.publish_keyed(
391            0,
392            Bytes::from_static(b"range"),
393            Some(Bytes::from_static(b"object")),
394        )
395        .await
396        .unwrap();
397        log.publish_keyed(0, Bytes::from_static(b"range"), None)
398            .await
399            .unwrap();
400        let (mut stream, _) = log.subscribe(vec![PartitionStart {
401            partition: 0,
402            start_offset: 0,
403        }]);
404
405        let value = stream.next().await.unwrap();
406        let tombstone = stream.next().await.unwrap();
407        assert!(value.key.as_deref() == Some(b"range".as_slice()));
408        assert!(!value.tombstone);
409        assert!(tombstone.key.as_deref() == Some(b"range".as_slice()));
410        assert!(tombstone.tombstone);
411    }
412
413    #[tokio::test]
414    async fn subscribe_replays_history_then_forwards_new_writes() {
415        let log = InProcessMetadataEventLog::new(1);
416        log.publish(0, Bytes::from_static(b"a")).await.unwrap();
417        log.publish(0, Bytes::from_static(b"b")).await.unwrap();
418        let (mut stream, _h) = log.subscribe(vec![PartitionStart {
419            partition: 0,
420            start_offset: 0,
421        }]);
422        let a = stream.next().await.unwrap();
423        let b = stream.next().await.unwrap();
424        assert!(a.payload.as_ref() == b"a");
425        assert!(b.payload.as_ref() == b"b");
426        log.publish(0, Bytes::from_static(b"c")).await.unwrap();
427        let c = stream.next().await.unwrap();
428        check!(c.payload.as_ref() == b"c");
429        check!(c.partition == 0);
430        check!(c.offset == 2);
431    }
432
433    #[tokio::test]
434    async fn subscribe_attached_after_history_still_sees_history() {
435        let log = InProcessMetadataEventLog::new(1);
436        for i in 0..5 {
437            log.publish(0, Bytes::copy_from_slice(&[i])).await.unwrap();
438        }
439        let (mut stream, _h) = log.subscribe(vec![PartitionStart {
440            partition: 0,
441            start_offset: 0,
442        }]);
443        for i in 0..5 {
444            let r = stream.next().await.unwrap();
445            assert!(r.payload.as_ref() == &[i]);
446            assert!(r.offset == i64::from(i));
447        }
448    }
449
450    #[tokio::test]
451    async fn lag_replays_exact_and_larger_bursts_once_in_order() {
452        use std::time::Duration;
453
454        for count in [1025_i64, 4097] {
455            let log = InProcessMetadataEventLog::new(1);
456            let (mut stream, _handle) = log.subscribe(vec![PartitionStart::new(0, 0)]);
457            for offset in 0..count {
458                let payload = Bytes::copy_from_slice(&offset.to_be_bytes());
459                assert!(log.publish(0, payload).await.unwrap() == offset);
460            }
461
462            time::timeout(Duration::from_secs(3), async {
463                for expected in 0..count {
464                    let record = stream.next().await.expect("retained record is replayed");
465                    assert!(record.offset == expected);
466                    assert!(record.payload.as_ref() == expected.to_be_bytes());
467                }
468            })
469            .await
470            .expect("burst replay completes");
471
472            // A new subscriber remains an independent retained-log control.
473            let (mut fresh, _fresh_handle) = log.subscribe(vec![PartitionStart::new(0, 0)]);
474            assert!(fresh.next().await.unwrap().offset == 0);
475        }
476    }
477
478    #[tokio::test]
479    async fn snapshot_then_live_delivery_has_no_overlap_or_gap() {
480        let log = InProcessMetadataEventLog::new(1);
481        for offset in 0..4_i64 {
482            log.publish(0, Bytes::copy_from_slice(&offset.to_be_bytes()))
483                .await
484                .unwrap();
485        }
486        let (mut stream, _handle) = log.subscribe(vec![PartitionStart::new(0, 2)]);
487        for offset in 4..8_i64 {
488            log.publish(0, Bytes::copy_from_slice(&offset.to_be_bytes()))
489                .await
490                .unwrap();
491        }
492
493        for expected in 2..8_i64 {
494            let record = stream.next().await.unwrap();
495            assert!(record.offset == expected);
496            assert!(record.payload.as_ref() == expected.to_be_bytes());
497        }
498    }
499
500    #[tokio::test]
501    async fn assignment_added_before_overflow_replays_its_backlog_and_live_events() {
502        let log = InProcessMetadataEventLog::new(2);
503        let (mut stream, handle) = log.subscribe(vec![PartitionStart::new(0, 0)]);
504        for offset in 0..4_i64 {
505            log.publish(1, Bytes::copy_from_slice(&offset.to_be_bytes()))
506                .await
507                .unwrap();
508        }
509        handle.add(PartitionStart::new(1, 1));
510        log.publish(1, Bytes::from_static(b"live")).await.unwrap();
511        for _ in 0..1025 {
512            log.publish(0, Bytes::from_static(b"overflow"))
513                .await
514                .unwrap();
515        }
516
517        let mut partition_zero = Vec::new();
518        let mut partition_one = Vec::new();
519        for _ in 0..1029 {
520            let record = stream.next().await.unwrap();
521            match record.partition {
522                0 => partition_zero.push(record.offset),
523                1 => partition_one.push((record.offset, record.payload)),
524                _ => panic!("unexpected partition"),
525            }
526        }
527        assert!(partition_zero == (0..1025).collect::<Vec<_>>());
528        assert!(
529            partition_one
530                == vec![
531                    (1, Bytes::copy_from_slice(&1_i64.to_be_bytes())),
532                    (2, Bytes::copy_from_slice(&2_i64.to_be_bytes())),
533                    (3, Bytes::copy_from_slice(&3_i64.to_be_bytes())),
534                    (4, Bytes::from_static(b"live")),
535                ]
536        );
537    }
538
539    #[tokio::test]
540    async fn publish_out_of_range_is_rejected() {
541        let log = InProcessMetadataEventLog::new(2);
542        let err = log.publish(5, Bytes::from_static(b"x")).await.unwrap_err();
543        assert!(matches!(err, MetadataLogError::PartitionOutOfRange { .. }));
544    }
545
546    #[tokio::test]
547    async fn two_subscribers_see_the_same_history() {
548        let log = InProcessMetadataEventLog::new(1);
549        log.publish(0, Bytes::from_static(b"a")).await.unwrap();
550        let (mut s1, _h1) = log.subscribe(vec![PartitionStart {
551            partition: 0,
552            start_offset: 0,
553        }]);
554        let (mut s2, _h2) = log.subscribe(vec![PartitionStart {
555            partition: 0,
556            start_offset: 0,
557        }]);
558        log.publish(0, Bytes::from_static(b"b")).await.unwrap();
559        for s in [&mut s1, &mut s2] {
560            assert!(s.next().await.unwrap().payload.as_ref() == b"a");
561            assert!(s.next().await.unwrap().payload.as_ref() == b"b");
562        }
563    }
564
565    #[tokio::test]
566    async fn subscribe_delivers_only_assigned_partitions_from_start_offset() {
567        #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
568        struct Delivered {
569            partition: i32,
570            offset: i64,
571            payload: Vec<u8>,
572        }
573        let log = InProcessMetadataEventLog::new(3);
574        // partition 0: a,b,c ; partition 1: x,y ; partition 2: z
575        for p0 in [b"a".as_slice(), b"b", b"c"] {
576            log.publish(0, Bytes::copy_from_slice(p0)).await.unwrap();
577        }
578        for p1 in [b"x".as_slice(), b"y"] {
579            log.publish(1, Bytes::copy_from_slice(p1)).await.unwrap();
580        }
581        log.publish(2, Bytes::from_static(b"z")).await.unwrap();
582
583        // Assign partition 0 from offset 1 and partition 1 from offset 0;
584        // partition 2 is NOT assigned.
585        let (mut stream, _h) =
586            log.subscribe(vec![PartitionStart::new(0, 1), PartitionStart::new(1, 0)]);
587
588        let delivered = |partition, offset, payload: &[u8]| Delivered {
589            partition,
590            offset,
591            payload: payload.to_vec(),
592        };
593        let mut got: Vec<Delivered> = Vec::new();
594        for _ in 0..3 {
595            let r = stream.next().await.unwrap();
596            got.push(delivered(r.partition, r.offset, &r.payload));
597        }
598        got.sort();
599        assert2::assert!(
600            got == vec![
601                delivered(0, 1, b"b"),
602                delivered(0, 2, b"c"),
603                delivered(1, 0, b"x"),
604            ]
605        );
606        // partition 1 offset 1 ("y") is the only remaining assigned record.
607        let r = stream.next().await.unwrap();
608        check!(r.partition == 1);
609        check!(r.offset == 1);
610        check!(r.payload.as_ref() == b"y");
611    }
612
613    #[tokio::test]
614    async fn live_appends_only_for_assigned_partitions() {
615        let log = InProcessMetadataEventLog::new(2);
616        let (mut stream, _h) = log.subscribe(vec![PartitionStart {
617            partition: 0,
618            start_offset: 0,
619        }]);
620        // Unassigned partition write must not appear.
621        log.publish(1, Bytes::from_static(b"skip")).await.unwrap();
622        log.publish(0, Bytes::from_static(b"keep")).await.unwrap();
623        let r = stream.next().await.unwrap();
624        check!(r.partition == 0);
625        check!(r.payload.as_ref() == b"keep");
626    }
627}