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]
389 pub fn stalled_partitions(&self) -> Vec<(PartitionId, Instant)> {
390 let mut out: Vec<_> = self
391 .trackers
392 .iter()
393 .filter_map(|(&p, t)| t.stalled_since().map(|since| (p, since)))
394 .collect();
395 out.sort_unstable_by_key(|&(p, _)| p);
396 out
397 }
398}
399
400#[cfg(all(test, not(loom)))]
401mod tests {
402 use super::*;
403
404 const P0: PartitionId = PartitionId(0);
405 const P1: PartitionId = PartitionId(1);
406
407 fn checkpointer(partitions: &[PartitionId]) -> (Checkpointer, AckIssuer) {
408 let mut cp = Checkpointer::new();
409 cp.begin_epoch(partitions, 1);
410 let issuer = cp.handle();
411 (cp, issuer)
412 }
413
414 #[test]
418 fn advance_counters_track_retired_batches() {
419 let (mut cp, mut issuer) = checkpointer(&[P0]);
420 let gate = cp.advance_handle(P0).expect("gate for a tracked partition");
421 drop(issuer.issue(P0, 9));
422 drop(issuer.issue(P0, 19));
423 cp.drain();
424 assert_eq!(cp.take_watermarks(), vec![(P0, 20)]);
425 assert_eq!(gate.get(), 2, "both retired batches counted");
426
427 issuer.issue(P0, 29).fail();
429 drop(issuer.issue(P0, 39));
430 cp.drain();
431 assert!(cp.take_watermarks().is_empty());
432 assert_eq!(gate.get(), 2, "a stalled head retires nothing");
433
434 cp.revoke(&[P0]);
436 assert!(cp.advance_handle(P0).is_none());
437 cp.begin_epoch(&[P0], 2);
438 let fresh = cp.advance_handle(P0).expect("fresh gate");
439 assert_eq!(fresh.get(), 0);
440 }
441
442 #[test]
443 fn issue_drain_take_happy_path() {
444 let (mut cp, mut issuer) = checkpointer(&[P0]);
445 drop(issuer.issue(P0, 99));
446 drop(issuer.issue(P0, 199));
447 let stats = cp.drain();
448 assert_eq!(stats.applied, 2);
449 assert_eq!(
450 stats,
451 DrainStats {
452 applied: 2,
453 ..Default::default()
454 }
455 );
456 assert_eq!(cp.take_watermarks(), vec![(P0, 200)]);
457 }
458
459 #[test]
460 fn extend_epoch_adds_partitions_without_disturbing_inflight_acks() {
461 let (mut cp, mut issuer) = checkpointer(&[P0]);
462 let ack = issuer.issue(P0, 99);
464 cp.extend_epoch(&[P1]).unwrap();
465 drop(ack);
467 drop(issuer.issue(P1, 9));
468 let stats = cp.drain();
469 assert_eq!(stats.applied, 2);
470 assert_eq!(stats.stale_epoch, 0);
471 assert_eq!(cp.take_watermarks(), vec![(P0, 100), (P1, 10)]);
472 }
473
474 #[test]
475 fn extend_epoch_rejects_a_live_partition() {
476 let (mut cp, _issuer) = checkpointer(&[P0]);
477 let err = cp.extend_epoch(&[P0]).unwrap_err();
478 assert!(err.reason.contains("already tracked"), "{err}");
479 }
480
481 #[test]
482 fn extend_epoch_rejects_a_partition_revoked_earlier_in_the_epoch() {
483 let (mut cp, mut issuer) = checkpointer(&[P0]);
490 drop(issuer.issue(P0, 9));
491 cp.drain();
492 cp.revoke(&[P0]);
493
494 let err = cp.extend_epoch(&[P0]).unwrap_err();
495 assert_eq!(err.component, "checkpoint");
496 assert!(
497 err.reason.contains("revoked earlier in this epoch"),
498 "{err}"
499 );
500
501 drop(issuer.issue(P0, 19));
505 let stats = cp.drain();
506 assert_eq!(stats.applied, 0);
507 assert_eq!(stats.stale_epoch, 2);
508
509 cp.begin_epoch(&[P0], 2);
511 cp.extend_epoch(&[P1]).unwrap();
512 }
513
514 #[test]
515 fn take_watermarks_is_empty_until_new_progress() {
516 let (mut cp, mut issuer) = checkpointer(&[P0]);
517 drop(issuer.issue(P0, 9));
518 cp.drain();
519 assert_eq!(cp.take_watermarks(), vec![(P0, 10)]);
520 assert_eq!(cp.take_watermarks(), vec![]);
521 drop(issuer.issue(P0, 19));
522 cp.drain();
523 assert_eq!(cp.take_watermarks(), vec![(P0, 20)]);
524 }
525
526 #[test]
527 fn out_of_order_acks_across_partitions() {
528 let (mut cp, mut issuer) = checkpointer(&[P0, P1]);
529 let a0 = issuer.issue(P0, 9);
530 let a1 = issuer.issue(P0, 19);
531 let b0 = issuer.issue(P1, 99);
532 drop(a1);
534 drop(b0);
535 cp.drain();
536 assert_eq!(cp.take_watermarks(), vec![(P1, 100)]);
537 assert_eq!(cp.pending(P0), 2);
538 drop(a0);
539 cp.drain();
540 assert_eq!(cp.take_watermarks(), vec![(P0, 20)]);
541 }
542
543 #[test]
544 fn failed_batch_stalls_partition_and_reports() {
545 let (mut cp, mut issuer) = checkpointer(&[P0]);
546 let bad = issuer.issue(P0, 9);
547 bad.fail();
548 drop(bad);
549 drop(issuer.issue(P0, 19));
550 cp.drain();
551 assert_eq!(cp.take_watermarks(), vec![]);
552 let stalled = cp.stalled_partitions();
553 assert_eq!(stalled.len(), 1);
554 assert_eq!(stalled[0].0, P0);
555 }
556
557 #[test]
558 fn stale_epoch_acks_are_discarded() {
559 let (mut cp, mut issuer) = checkpointer(&[P0]);
560 let old = issuer.issue(P0, 9);
561 cp.begin_epoch(&[P0], 2);
562 drop(old); let stats = cp.drain();
564 assert_eq!(stats.applied, 0);
565 assert_eq!(stats.stale_epoch, 2);
567 assert_eq!(cp.take_watermarks(), vec![]);
568
569 drop(issuer.issue(P0, 49));
571 let stats = cp.drain();
572 assert_eq!(stats.applied, 1);
573 assert_eq!(cp.take_watermarks(), vec![(P0, 50)]);
574 }
575
576 #[test]
577 fn revoke_mid_flight_discards_later_acks() {
578 let (mut cp, mut issuer) = checkpointer(&[P0, P1]);
579 let in_flight = issuer.issue(P1, 9);
580 cp.drain(); cp.revoke(&[P1]);
582 drop(in_flight);
583 let stats = cp.drain();
584 assert_eq!(stats.stale_epoch, 1);
585 assert_eq!(cp.take_watermarks(), vec![]);
586 assert_eq!(cp.pending(P1), 0);
587 }
588
589 #[test]
590 fn registration_and_ack_in_same_drain() {
591 let (mut cp, mut issuer) = checkpointer(&[P0]);
594 drop(issuer.issue(P0, 9));
595 let stats = cp.drain();
596 assert_eq!(stats.applied, 1);
597 assert_eq!(stats.unknown, 0);
598 assert_eq!(cp.take_watermarks(), vec![(P0, 10)]);
599 }
600
601 #[test]
602 fn cross_thread_issue_and_resolve() {
603 let (mut cp, issuer) = checkpointer(&[P0, P1]);
604 let handles: Vec<_> = [P0, P1]
605 .into_iter()
606 .map(|p| {
607 let mut issuer = issuer.clone();
608 std::thread::spawn(move || {
609 for i in 0..100i64 {
610 drop(issuer.issue(p, (i + 1) * 10 - 1));
611 }
612 })
613 })
614 .collect();
615 for h in handles {
616 h.join().unwrap();
617 }
618 let stats = cp.drain();
619 assert_eq!(stats.applied, 200);
620 assert_eq!(stats.unknown, 0);
621 assert_eq!(cp.take_watermarks(), vec![(P0, 1000), (P1, 1000)]);
622 }
623
624 #[test]
625 fn pending_counts_feed_backpressure() {
626 let (mut cp, mut issuer) = checkpointer(&[P0, P1]);
627 let held: Vec<_> = (0..5).map(|i| issuer.issue(P0, i)).collect();
628 drop(issuer.issue(P1, 9));
629 cp.drain();
630 assert_eq!(cp.pending(P0), 5);
631 assert_eq!(cp.max_pending(), 5);
632 drop(held);
633 cp.drain();
634 let _ = cp.take_watermarks();
635 assert_eq!(cp.max_pending(), 0);
636 }
637
638 #[test]
639 #[should_panic(expected = "strictly increasing")]
640 fn epoch_regression_panics() {
641 let mut cp = Checkpointer::new();
642 cp.begin_epoch(&[P0], 5);
643 cp.begin_epoch(&[P0], 5);
644 }
645}
646
647#[cfg(all(test, not(loom)))]
648mod proptests {
649 use super::*;
650 use proptest::prelude::*;
651
652 #[derive(Clone, Debug)]
653 enum Op {
654 Issue { partition: u8, fail: bool },
655 ResolveOldest,
656 Rebalance { partitions: Vec<u8> },
657 DrainAndTake,
658 }
659
660 fn ops() -> impl Strategy<Value = Vec<Op>> {
661 prop::collection::vec(
662 prop_oneof![
663 (0..3u8, any::<bool>()).prop_map(|(partition, fail)| Op::Issue { partition, fail }),
664 Just(Op::ResolveOldest),
665 prop::collection::vec(0..3u8, 1..3)
666 .prop_map(|partitions| Op::Rebalance { partitions }),
667 Just(Op::DrainAndTake),
668 ],
669 0..120,
670 )
671 }
672
673 proptest! {
674 #[test]
678 fn epoch_churn_never_leaks_stale_acks(ops in ops()) {
679 let mut cp = Checkpointer::new();
680 let mut epoch = 1u32;
681 let mut assigned: Vec<PartitionId> = vec![PartitionId(0), PartitionId(1), PartitionId(2)];
682 cp.begin_epoch(&assigned, epoch);
683 let mut issuer = cp.handle();
684 let mut offsets: std::collections::HashMap<PartitionId, i64> =
685 std::collections::HashMap::new();
686 let mut held: std::collections::VecDeque<(AckRef, u32, bool)> =
688 std::collections::VecDeque::new();
689 let mut last_watermark: std::collections::HashMap<PartitionId, i64> =
690 std::collections::HashMap::new();
691
692 for op in ops {
693 match op {
694 Op::Issue { partition, fail } => {
695 let p = PartitionId(u32::from(partition));
696 if !assigned.contains(&p) {
697 continue;
698 }
699 let next = offsets.entry(p).or_insert(0);
700 *next += 10;
701 let ack = issuer.issue(p, *next - 1);
702 if fail {
703 ack.fail();
704 }
705 held.push_back((ack, epoch, fail));
706 }
707 Op::ResolveOldest => {
708 held.pop_front(); }
710 Op::Rebalance { partitions } => {
711 epoch += 1;
712 assigned = partitions
713 .into_iter()
714 .map(|p| PartitionId(u32::from(p)))
715 .collect::<std::collections::BTreeSet<_>>()
716 .into_iter()
717 .collect();
718 cp.begin_epoch(&assigned, epoch);
719 offsets.clear();
722 last_watermark.clear();
723 }
724 Op::DrainAndTake => {
725 cp.drain();
726 for (p, w) in cp.take_watermarks() {
727 prop_assert!(
728 assigned.contains(&p),
729 "watermark for unassigned partition {p:?}"
730 );
731 if let Some(&prev) = last_watermark.get(&p) {
732 prop_assert!(w > prev, "watermark not monotonic for {p:?}");
733 }
734 last_watermark.insert(p, w);
735 }
736 }
737 }
738 }
739
740 let stale_epochs: Vec<u32> =
743 held.iter().map(|&(_, e, _)| e).filter(|&e| e != epoch).collect();
744 held.clear();
745 let stats = cp.drain();
746 prop_assert!(stats.unknown == 0, "driver-bug resolutions: {stats:?}");
747 for (p, w) in cp.take_watermarks() {
748 prop_assert!(assigned.contains(&p));
749 if let Some(&prev) = last_watermark.get(&p) {
750 prop_assert!(w > prev);
751 }
752 }
753 if !stale_epochs.is_empty() {
755 prop_assert!(stats.stale_epoch > 0);
756 }
757 }
758 }
759}