1use super::ack::AckTx;
13use super::gate::AdvanceCounter;
14use super::tracker::{PartitionTracker, ResolveOutcome};
15use super::{AckMsg, AckRef, BatchId};
16use crate::error::FatalError;
17use crate::record::PartitionId;
18use std::collections::{HashMap, HashSet};
19use std::sync::Arc;
20use std::sync::atomic::{AtomicU32, Ordering};
21use std::time::Instant;
22
23#[derive(Clone, Copy, Debug)]
25struct Registration {
26 id: BatchId,
27 last_offset: i64,
28}
29
30#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
32pub struct DrainStats {
33 pub applied: usize,
35 pub stale_epoch: usize,
38 pub duplicates: usize,
40 pub unknown: usize,
42}
43
44#[derive(Debug)]
52pub struct AckIssuer {
53 ack_tx: crossbeam_channel::Sender<AckMsg>,
54 reg_tx: crossbeam_channel::Sender<Registration>,
55 shared_epoch: Arc<AtomicU32>,
56 local_epoch: u32,
57 seqs: HashMap<PartitionId, u64>,
58}
59
60impl Clone for AckIssuer {
61 fn clone(&self) -> Self {
62 AckIssuer {
63 ack_tx: self.ack_tx.clone(),
64 reg_tx: self.reg_tx.clone(),
65 shared_epoch: Arc::clone(&self.shared_epoch),
66 local_epoch: self.local_epoch,
67 seqs: HashMap::new(),
68 }
69 }
70}
71
72impl AckIssuer {
73 pub fn issue(&mut self, partition: PartitionId, last_offset: i64) -> AckRef {
79 let epoch = self.shared_epoch.load(Ordering::Acquire);
80 if epoch != self.local_epoch {
81 self.local_epoch = epoch;
83 self.seqs.clear();
84 }
85 let seq_slot = self.seqs.entry(partition).or_insert(0);
86 let seq = *seq_slot;
87 *seq_slot += 1;
88
89 let id = BatchId {
90 partition,
91 epoch,
92 seq,
93 };
94 let _ = self.reg_tx.send(Registration { id, last_offset });
98 AckRef::new(id, last_offset, AckTx::Channel(self.ack_tx.clone()))
99 }
100}
101
102#[derive(Debug)]
121pub struct Checkpointer {
122 ack_tx: crossbeam_channel::Sender<AckMsg>,
123 ack_rx: crossbeam_channel::Receiver<AckMsg>,
124 reg_tx: crossbeam_channel::Sender<Registration>,
125 reg_rx: crossbeam_channel::Receiver<Registration>,
126 shared_epoch: Arc<AtomicU32>,
127 epoch: u32,
128 trackers: HashMap<PartitionId, PartitionTracker>,
129 gates: HashMap<PartitionId, AdvanceCounter>,
133 admitted: HashSet<PartitionId>,
137}
138
139impl Default for Checkpointer {
140 fn default() -> Self {
141 Self::new()
142 }
143}
144
145impl Checkpointer {
146 #[must_use]
151 pub fn new() -> Self {
152 let (ack_tx, ack_rx) = crossbeam_channel::unbounded();
153 let (reg_tx, reg_rx) = crossbeam_channel::unbounded();
154 Checkpointer {
155 ack_tx,
156 ack_rx,
157 reg_tx,
158 reg_rx,
159 shared_epoch: Arc::new(AtomicU32::new(0)),
160 epoch: 0,
161 trackers: HashMap::new(),
162 gates: HashMap::new(),
163 admitted: HashSet::new(),
164 }
165 }
166
167 #[must_use]
169 pub fn handle(&self) -> AckIssuer {
170 AckIssuer {
171 ack_tx: self.ack_tx.clone(),
172 reg_tx: self.reg_tx.clone(),
173 shared_epoch: Arc::clone(&self.shared_epoch),
174 local_epoch: self.shared_epoch.load(Ordering::Acquire),
175 seqs: HashMap::new(),
176 }
177 }
178
179 pub fn begin_epoch(&mut self, partitions: &[PartitionId], epoch: u32) {
188 assert!(
189 epoch > self.epoch || (self.epoch == 0 && self.trackers.is_empty()),
190 "assignment epochs must be strictly increasing: {} -> {epoch}",
191 self.epoch
192 );
193 self.epoch = epoch;
194 self.trackers = partitions
195 .iter()
196 .map(|&p| (p, PartitionTracker::new()))
197 .collect();
198 self.gates = partitions
199 .iter()
200 .map(|&p| (p, AdvanceCounter::new()))
201 .collect();
202 self.admitted = partitions.iter().copied().collect();
206 self.shared_epoch.store(epoch, Ordering::Release);
209 }
210
211 pub fn extend_epoch(&mut self, partitions: &[PartitionId]) -> Result<(), FatalError> {
236 for &p in partitions {
239 if self.admitted.contains(&p) {
240 let live = if self.trackers.contains_key(&p) {
241 "is already tracked"
242 } else {
243 "was revoked earlier in this epoch"
244 };
245 return Err(FatalError {
246 component: "checkpoint".into(),
247 reason: format!(
248 "additive assignment reused partition {} which {live}; every \
249 added lane must carry a partition never seen in this epoch \
250 (a returning partition needs a new epoch)",
251 p.0
252 ),
253 });
254 }
255 }
256 for &p in partitions {
257 self.trackers.insert(p, PartitionTracker::new());
258 self.gates.insert(p, AdvanceCounter::new());
259 self.admitted.insert(p);
260 }
261 Ok(())
262 }
263
264 pub fn revoke(&mut self, partitions: &[PartitionId]) {
268 for p in partitions {
269 self.trackers.remove(p);
270 self.gates.remove(p);
271 }
272 }
273
274 #[must_use]
278 pub(crate) fn advance_handle(&self, partition: PartitionId) -> Option<AdvanceCounter> {
279 self.gates.get(&partition).cloned()
280 }
281
282 pub fn drain(&mut self) -> DrainStats {
290 let mut stats = DrainStats::default();
291 self.drain_registrations(&mut stats);
292
293 let mut deferred = Vec::new();
294 while let Ok(msg) = self.ack_rx.try_recv() {
295 self.apply(msg, &mut stats, Some(&mut deferred));
296 }
297
298 if !deferred.is_empty() {
299 self.drain_registrations(&mut stats);
300 for msg in deferred {
301 self.apply(msg, &mut stats, None);
302 }
303 }
304 stats
305 }
306
307 fn drain_registrations(&mut self, stats: &mut DrainStats) {
308 while let Ok(reg) = self.reg_rx.try_recv() {
309 if reg.id.epoch != self.epoch {
310 stats.stale_epoch += 1;
311 continue;
312 }
313 match self.trackers.get_mut(®.id.partition) {
314 Some(tracker) => tracker.register(reg.id.seq, reg.last_offset),
315 None => stats.stale_epoch += 1,
317 }
318 }
319 }
320
321 fn apply(&mut self, msg: AckMsg, stats: &mut DrainStats, defer: Option<&mut Vec<AckMsg>>) {
322 if msg.id.epoch != self.epoch {
323 stats.stale_epoch += 1;
324 return;
325 }
326 let Some(tracker) = self.trackers.get_mut(&msg.id.partition) else {
327 stats.stale_epoch += 1;
328 return;
329 };
330 match tracker.resolve(msg.id.seq, msg.status) {
331 ResolveOutcome::Applied => stats.applied += 1,
332 ResolveOutcome::Duplicate | ResolveOutcome::AlreadyAdvanced => stats.duplicates += 1,
333 ResolveOutcome::Unregistered => match defer {
334 Some(deferred) => deferred.push(msg),
335 None => {
336 debug_assert!(false, "resolution without registration: {:?}", msg.id);
337 stats.unknown += 1;
338 }
339 },
340 }
341 }
342
343 #[must_use]
347 pub fn take_watermarks(&mut self) -> Vec<(PartitionId, i64)> {
348 let mut out = Vec::new();
349 for (&p, t) in &mut self.trackers {
350 let before = t.pending();
351 let watermark = t.advance();
352 let retired = before - t.pending();
355 if retired > 0
356 && let Some(gate) = self.gates.get(&p)
357 {
358 gate.add(retired as u64);
359 }
360 if let Some(w) = watermark {
361 out.push((p, w));
362 }
363 }
364 out.sort_unstable_by_key(|&(p, _)| p);
365 out
366 }
367
368 #[must_use]
370 pub fn pending(&self, partition: PartitionId) -> usize {
371 self.trackers
372 .get(&partition)
373 .map_or(0, PartitionTracker::pending)
374 }
375
376 #[must_use]
378 pub fn max_pending(&self) -> usize {
379 self.trackers
380 .values()
381 .map(PartitionTracker::pending)
382 .max()
383 .unwrap_or(0)
384 }
385
386 #[must_use]
393 pub fn pending_by_partition(&self) -> Vec<(PartitionId, usize)> {
394 let mut out: Vec<_> = self
395 .trackers
396 .iter()
397 .map(|(&p, t)| (p, t.pending()))
398 .collect();
399 out.sort_unstable_by_key(|&(p, _)| p);
400 out
401 }
402
403 #[must_use]
406 pub fn stalled_partitions(&self) -> Vec<(PartitionId, Instant)> {
407 let mut out: Vec<_> = self
408 .trackers
409 .iter()
410 .filter_map(|(&p, t)| t.stalled_since().map(|since| (p, since)))
411 .collect();
412 out.sort_unstable_by_key(|&(p, _)| p);
413 out
414 }
415}
416
417#[cfg(all(test, not(loom)))]
418mod tests {
419 use super::*;
420
421 const P0: PartitionId = PartitionId(0);
422 const P1: PartitionId = PartitionId(1);
423
424 fn checkpointer(partitions: &[PartitionId]) -> (Checkpointer, AckIssuer) {
425 let mut cp = Checkpointer::new();
426 cp.begin_epoch(partitions, 1);
427 let issuer = cp.handle();
428 (cp, issuer)
429 }
430
431 #[test]
435 fn advance_counters_track_retired_batches() {
436 let (mut cp, mut issuer) = checkpointer(&[P0]);
437 let gate = cp.advance_handle(P0).expect("gate for a tracked partition");
438 drop(issuer.issue(P0, 9));
439 drop(issuer.issue(P0, 19));
440 cp.drain();
441 assert_eq!(cp.take_watermarks(), vec![(P0, 20)]);
442 assert_eq!(gate.get(), 2, "both retired batches counted");
443
444 issuer.issue(P0, 29).fail();
446 drop(issuer.issue(P0, 39));
447 cp.drain();
448 assert!(cp.take_watermarks().is_empty());
449 assert_eq!(gate.get(), 2, "a stalled head retires nothing");
450
451 cp.revoke(&[P0]);
453 assert!(cp.advance_handle(P0).is_none());
454 cp.begin_epoch(&[P0], 2);
455 let fresh = cp.advance_handle(P0).expect("fresh gate");
456 assert_eq!(fresh.get(), 0);
457 }
458
459 #[test]
460 fn issue_drain_take_happy_path() {
461 let (mut cp, mut issuer) = checkpointer(&[P0]);
462 drop(issuer.issue(P0, 99));
463 drop(issuer.issue(P0, 199));
464 let stats = cp.drain();
465 assert_eq!(stats.applied, 2);
466 assert_eq!(
467 stats,
468 DrainStats {
469 applied: 2,
470 ..Default::default()
471 }
472 );
473 assert_eq!(cp.take_watermarks(), vec![(P0, 200)]);
474 }
475
476 #[test]
477 fn extend_epoch_adds_partitions_without_disturbing_inflight_acks() {
478 let (mut cp, mut issuer) = checkpointer(&[P0]);
479 let ack = issuer.issue(P0, 99);
481 cp.extend_epoch(&[P1]).unwrap();
482 drop(ack);
484 drop(issuer.issue(P1, 9));
485 let stats = cp.drain();
486 assert_eq!(stats.applied, 2);
487 assert_eq!(stats.stale_epoch, 0);
488 assert_eq!(cp.take_watermarks(), vec![(P0, 100), (P1, 10)]);
489 }
490
491 #[test]
492 fn extend_epoch_rejects_a_live_partition() {
493 let (mut cp, _issuer) = checkpointer(&[P0]);
494 let err = cp.extend_epoch(&[P0]).unwrap_err();
495 assert!(err.reason.contains("already tracked"), "{err}");
496 }
497
498 #[test]
499 fn extend_epoch_rejects_a_partition_revoked_earlier_in_the_epoch() {
500 let (mut cp, mut issuer) = checkpointer(&[P0]);
507 drop(issuer.issue(P0, 9));
508 cp.drain();
509 cp.revoke(&[P0]);
510
511 let err = cp.extend_epoch(&[P0]).unwrap_err();
512 assert_eq!(err.component, "checkpoint");
513 assert!(
514 err.reason.contains("revoked earlier in this epoch"),
515 "{err}"
516 );
517
518 drop(issuer.issue(P0, 19));
522 let stats = cp.drain();
523 assert_eq!(stats.applied, 0);
524 assert_eq!(stats.stale_epoch, 2);
525
526 cp.begin_epoch(&[P0], 2);
528 cp.extend_epoch(&[P1]).unwrap();
529 }
530
531 #[test]
532 fn take_watermarks_is_empty_until_new_progress() {
533 let (mut cp, mut issuer) = checkpointer(&[P0]);
534 drop(issuer.issue(P0, 9));
535 cp.drain();
536 assert_eq!(cp.take_watermarks(), vec![(P0, 10)]);
537 assert_eq!(cp.take_watermarks(), vec![]);
538 drop(issuer.issue(P0, 19));
539 cp.drain();
540 assert_eq!(cp.take_watermarks(), vec![(P0, 20)]);
541 }
542
543 #[test]
544 fn out_of_order_acks_across_partitions() {
545 let (mut cp, mut issuer) = checkpointer(&[P0, P1]);
546 let a0 = issuer.issue(P0, 9);
547 let a1 = issuer.issue(P0, 19);
548 let b0 = issuer.issue(P1, 99);
549 drop(a1);
551 drop(b0);
552 cp.drain();
553 assert_eq!(cp.take_watermarks(), vec![(P1, 100)]);
554 assert_eq!(cp.pending(P0), 2);
555 drop(a0);
556 cp.drain();
557 assert_eq!(cp.take_watermarks(), vec![(P0, 20)]);
558 }
559
560 #[test]
561 fn failed_batch_stalls_partition_and_reports() {
562 let (mut cp, mut issuer) = checkpointer(&[P0]);
563 let bad = issuer.issue(P0, 9);
564 bad.fail();
565 drop(bad);
566 drop(issuer.issue(P0, 19));
567 cp.drain();
568 assert_eq!(cp.take_watermarks(), vec![]);
569 let stalled = cp.stalled_partitions();
570 assert_eq!(stalled.len(), 1);
571 assert_eq!(stalled[0].0, P0);
572 }
573
574 #[test]
575 fn stale_epoch_acks_are_discarded() {
576 let (mut cp, mut issuer) = checkpointer(&[P0]);
577 let old = issuer.issue(P0, 9);
578 cp.begin_epoch(&[P0], 2);
579 drop(old); let stats = cp.drain();
581 assert_eq!(stats.applied, 0);
582 assert_eq!(stats.stale_epoch, 2);
584 assert_eq!(cp.take_watermarks(), vec![]);
585
586 drop(issuer.issue(P0, 49));
588 let stats = cp.drain();
589 assert_eq!(stats.applied, 1);
590 assert_eq!(cp.take_watermarks(), vec![(P0, 50)]);
591 }
592
593 #[test]
594 fn revoke_mid_flight_discards_later_acks() {
595 let (mut cp, mut issuer) = checkpointer(&[P0, P1]);
596 let in_flight = issuer.issue(P1, 9);
597 cp.drain(); cp.revoke(&[P1]);
599 drop(in_flight);
600 let stats = cp.drain();
601 assert_eq!(stats.stale_epoch, 1);
602 assert_eq!(cp.take_watermarks(), vec![]);
603 assert_eq!(cp.pending(P1), 0);
604 }
605
606 #[test]
607 fn registration_and_ack_in_same_drain() {
608 let (mut cp, mut issuer) = checkpointer(&[P0]);
611 drop(issuer.issue(P0, 9));
612 let stats = cp.drain();
613 assert_eq!(stats.applied, 1);
614 assert_eq!(stats.unknown, 0);
615 assert_eq!(cp.take_watermarks(), vec![(P0, 10)]);
616 }
617
618 #[test]
619 fn cross_thread_issue_and_resolve() {
620 let (mut cp, issuer) = checkpointer(&[P0, P1]);
621 let handles: Vec<_> = [P0, P1]
622 .into_iter()
623 .map(|p| {
624 let mut issuer = issuer.clone();
625 std::thread::spawn(move || {
626 for i in 0..100i64 {
627 drop(issuer.issue(p, (i + 1) * 10 - 1));
628 }
629 })
630 })
631 .collect();
632 for h in handles {
633 h.join().unwrap();
634 }
635 let stats = cp.drain();
636 assert_eq!(stats.applied, 200);
637 assert_eq!(stats.unknown, 0);
638 assert_eq!(cp.take_watermarks(), vec![(P0, 1000), (P1, 1000)]);
639 }
640
641 #[test]
642 fn pending_counts_feed_backpressure() {
643 let (mut cp, mut issuer) = checkpointer(&[P0, P1]);
644 let held: Vec<_> = (0..5).map(|i| issuer.issue(P0, i)).collect();
645 drop(issuer.issue(P1, 9));
646 cp.drain();
647 assert_eq!(cp.pending(P0), 5);
648 assert_eq!(cp.max_pending(), 5);
649 drop(held);
650 cp.drain();
651 let _ = cp.take_watermarks();
652 assert_eq!(cp.max_pending(), 0);
653 }
654
655 #[test]
660 fn pending_by_partition_lists_every_tracked_partition() {
661 let (mut cp, mut issuer) = checkpointer(&[P0, P1]);
662 let held: Vec<_> = (0..3).map(|i| issuer.issue(P0, i)).collect();
663 cp.drain();
664 assert_eq!(cp.pending_by_partition(), vec![(P0, 3), (P1, 0)]);
665 assert_eq!(
666 cp.max_pending(),
667 cp.pending_by_partition()
668 .iter()
669 .map(|&(_, n)| n)
670 .max()
671 .unwrap_or(0),
672 "the aggregate is the max of the per-partition counts"
673 );
674
675 drop(held);
678 cp.drain();
679 let _ = cp.take_watermarks();
680 cp.revoke(&[P0]);
681 assert_eq!(cp.pending_by_partition(), vec![(P1, 0)]);
682 }
683
684 #[test]
685 #[should_panic(expected = "strictly increasing")]
686 fn epoch_regression_panics() {
687 let mut cp = Checkpointer::new();
688 cp.begin_epoch(&[P0], 5);
689 cp.begin_epoch(&[P0], 5);
690 }
691}
692
693#[cfg(all(test, not(loom)))]
694mod proptests {
695 use super::*;
696 use proptest::prelude::*;
697
698 #[derive(Clone, Debug)]
699 enum Op {
700 Issue { partition: u8, fail: bool },
701 ResolveOldest,
702 Rebalance { partitions: Vec<u8> },
703 DrainAndTake,
704 }
705
706 fn ops() -> impl Strategy<Value = Vec<Op>> {
707 prop::collection::vec(
708 prop_oneof![
709 (0..3u8, any::<bool>()).prop_map(|(partition, fail)| Op::Issue { partition, fail }),
710 Just(Op::ResolveOldest),
711 prop::collection::vec(0..3u8, 1..3)
712 .prop_map(|partitions| Op::Rebalance { partitions }),
713 Just(Op::DrainAndTake),
714 ],
715 0..120,
716 )
717 }
718
719 proptest! {
720 #[test]
724 fn epoch_churn_never_leaks_stale_acks(ops in ops()) {
725 let mut cp = Checkpointer::new();
726 let mut epoch = 1u32;
727 let mut assigned: Vec<PartitionId> = vec![PartitionId(0), PartitionId(1), PartitionId(2)];
728 cp.begin_epoch(&assigned, epoch);
729 let mut issuer = cp.handle();
730 let mut offsets: std::collections::HashMap<PartitionId, i64> =
731 std::collections::HashMap::new();
732 let mut held: std::collections::VecDeque<(AckRef, u32, bool)> =
734 std::collections::VecDeque::new();
735 let mut last_watermark: std::collections::HashMap<PartitionId, i64> =
736 std::collections::HashMap::new();
737
738 for op in ops {
739 match op {
740 Op::Issue { partition, fail } => {
741 let p = PartitionId(u32::from(partition));
742 if !assigned.contains(&p) {
743 continue;
744 }
745 let next = offsets.entry(p).or_insert(0);
746 *next += 10;
747 let ack = issuer.issue(p, *next - 1);
748 if fail {
749 ack.fail();
750 }
751 held.push_back((ack, epoch, fail));
752 }
753 Op::ResolveOldest => {
754 held.pop_front(); }
756 Op::Rebalance { partitions } => {
757 epoch += 1;
758 assigned = partitions
759 .into_iter()
760 .map(|p| PartitionId(u32::from(p)))
761 .collect::<std::collections::BTreeSet<_>>()
762 .into_iter()
763 .collect();
764 cp.begin_epoch(&assigned, epoch);
765 offsets.clear();
768 last_watermark.clear();
769 }
770 Op::DrainAndTake => {
771 cp.drain();
772 for (p, w) in cp.take_watermarks() {
773 prop_assert!(
774 assigned.contains(&p),
775 "watermark for unassigned partition {p:?}"
776 );
777 if let Some(&prev) = last_watermark.get(&p) {
778 prop_assert!(w > prev, "watermark not monotonic for {p:?}");
779 }
780 last_watermark.insert(p, w);
781 }
782 }
783 }
784 }
785
786 let stale_epochs: Vec<u32> =
789 held.iter().map(|&(_, e, _)| e).filter(|&e| e != epoch).collect();
790 held.clear();
791 let stats = cp.drain();
792 prop_assert!(stats.unknown == 0, "driver-bug resolutions: {stats:?}");
793 for (p, w) in cp.take_watermarks() {
794 prop_assert!(assigned.contains(&p));
795 if let Some(&prev) = last_watermark.get(&p) {
796 prop_assert!(w > prev);
797 }
798 }
799 if !stale_epochs.is_empty() {
801 prop_assert!(stats.stale_epoch > 0);
802 }
803 }
804 }
805}