1use {
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
35pub struct InProcessMetadataEventLog {
39 inner: Arc<InProcessInner>,
40}
41
42#[derive(Debug, Clone, Copy)]
44struct PartitionCursor {
45 next: i64,
48 via_inject: bool,
57}
58
59struct SubscriptionState {
63 assigned: Mutex<HashMap<i32, PartitionCursor>>,
66 inject: mpsc::UnboundedSender<MetadataEventRecord>,
69}
70
71struct InProcessInner {
72 log: Mutex<Vec<Vec<MetadataEventRecord>>>,
74 tx: broadcast::Sender<MetadataEventRecord>,
76 partition_count: i32,
78 subscriptions: Mutex<HashMap<u64, Arc<SubscriptionState>>>,
81 next_sub_id: AtomicU64,
83}
84
85impl InProcessMetadataEventLog {
86 #[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 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 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 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 let guard = self.log();
186 let rx = self.inner.tx.subscribe();
187
188 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 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 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 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 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 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 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 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 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 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}