1use super::ack::AckTx;
13use super::tracker::{PartitionTracker, ResolveOutcome};
14use super::{AckMsg, AckRef, BatchId};
15use crate::record::PartitionId;
16use std::collections::HashMap;
17use std::sync::Arc;
18use std::sync::atomic::{AtomicU32, Ordering};
19use std::time::Instant;
20
21#[derive(Clone, Copy, Debug)]
23struct Registration {
24 id: BatchId,
25 last_offset: i64,
26}
27
28#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
30pub struct DrainStats {
31 pub applied: usize,
33 pub stale_epoch: usize,
36 pub duplicates: usize,
38 pub unknown: usize,
40}
41
42#[derive(Debug)]
50pub struct AckIssuer {
51 ack_tx: crossbeam_channel::Sender<AckMsg>,
52 reg_tx: crossbeam_channel::Sender<Registration>,
53 shared_epoch: Arc<AtomicU32>,
54 local_epoch: u32,
55 seqs: HashMap<PartitionId, u64>,
56}
57
58impl Clone for AckIssuer {
59 fn clone(&self) -> Self {
60 AckIssuer {
61 ack_tx: self.ack_tx.clone(),
62 reg_tx: self.reg_tx.clone(),
63 shared_epoch: Arc::clone(&self.shared_epoch),
64 local_epoch: self.local_epoch,
65 seqs: HashMap::new(),
66 }
67 }
68}
69
70impl AckIssuer {
71 pub fn issue(&mut self, partition: PartitionId, last_offset: i64) -> AckRef {
77 let epoch = self.shared_epoch.load(Ordering::Acquire);
78 if epoch != self.local_epoch {
79 self.local_epoch = epoch;
81 self.seqs.clear();
82 }
83 let seq_slot = self.seqs.entry(partition).or_insert(0);
84 let seq = *seq_slot;
85 *seq_slot += 1;
86
87 let id = BatchId {
88 partition,
89 epoch,
90 seq,
91 };
92 let _ = self.reg_tx.send(Registration { id, last_offset });
96 AckRef::new(id, last_offset, AckTx::Channel(self.ack_tx.clone()))
97 }
98}
99
100#[derive(Debug)]
119pub struct Checkpointer {
120 ack_tx: crossbeam_channel::Sender<AckMsg>,
121 ack_rx: crossbeam_channel::Receiver<AckMsg>,
122 reg_tx: crossbeam_channel::Sender<Registration>,
123 reg_rx: crossbeam_channel::Receiver<Registration>,
124 shared_epoch: Arc<AtomicU32>,
125 epoch: u32,
126 trackers: HashMap<PartitionId, PartitionTracker>,
127}
128
129impl Default for Checkpointer {
130 fn default() -> Self {
131 Self::new()
132 }
133}
134
135impl Checkpointer {
136 #[must_use]
141 pub fn new() -> Self {
142 let (ack_tx, ack_rx) = crossbeam_channel::unbounded();
143 let (reg_tx, reg_rx) = crossbeam_channel::unbounded();
144 Checkpointer {
145 ack_tx,
146 ack_rx,
147 reg_tx,
148 reg_rx,
149 shared_epoch: Arc::new(AtomicU32::new(0)),
150 epoch: 0,
151 trackers: HashMap::new(),
152 }
153 }
154
155 #[must_use]
157 pub fn handle(&self) -> AckIssuer {
158 AckIssuer {
159 ack_tx: self.ack_tx.clone(),
160 reg_tx: self.reg_tx.clone(),
161 shared_epoch: Arc::clone(&self.shared_epoch),
162 local_epoch: self.shared_epoch.load(Ordering::Acquire),
163 seqs: HashMap::new(),
164 }
165 }
166
167 pub fn begin_epoch(&mut self, partitions: &[PartitionId], epoch: u32) {
176 assert!(
177 epoch > self.epoch || (self.epoch == 0 && self.trackers.is_empty()),
178 "assignment epochs must be strictly increasing: {} -> {epoch}",
179 self.epoch
180 );
181 self.epoch = epoch;
182 self.trackers = partitions
183 .iter()
184 .map(|&p| (p, PartitionTracker::new()))
185 .collect();
186 self.shared_epoch.store(epoch, Ordering::Release);
189 }
190
191 pub fn revoke(&mut self, partitions: &[PartitionId]) {
195 for p in partitions {
196 self.trackers.remove(p);
197 }
198 }
199
200 pub fn drain(&mut self) -> DrainStats {
208 let mut stats = DrainStats::default();
209 self.drain_registrations(&mut stats);
210
211 let mut deferred = Vec::new();
212 while let Ok(msg) = self.ack_rx.try_recv() {
213 self.apply(msg, &mut stats, Some(&mut deferred));
214 }
215
216 if !deferred.is_empty() {
217 self.drain_registrations(&mut stats);
218 for msg in deferred {
219 self.apply(msg, &mut stats, None);
220 }
221 }
222 stats
223 }
224
225 fn drain_registrations(&mut self, stats: &mut DrainStats) {
226 while let Ok(reg) = self.reg_rx.try_recv() {
227 if reg.id.epoch != self.epoch {
228 stats.stale_epoch += 1;
229 continue;
230 }
231 match self.trackers.get_mut(®.id.partition) {
232 Some(tracker) => tracker.register(reg.id.seq, reg.last_offset),
233 None => stats.stale_epoch += 1,
235 }
236 }
237 }
238
239 fn apply(&mut self, msg: AckMsg, stats: &mut DrainStats, defer: Option<&mut Vec<AckMsg>>) {
240 if msg.id.epoch != self.epoch {
241 stats.stale_epoch += 1;
242 return;
243 }
244 let Some(tracker) = self.trackers.get_mut(&msg.id.partition) else {
245 stats.stale_epoch += 1;
246 return;
247 };
248 match tracker.resolve(msg.id.seq, msg.status) {
249 ResolveOutcome::Applied => stats.applied += 1,
250 ResolveOutcome::Duplicate | ResolveOutcome::AlreadyAdvanced => stats.duplicates += 1,
251 ResolveOutcome::Unregistered => match defer {
252 Some(deferred) => deferred.push(msg),
253 None => {
254 debug_assert!(false, "resolution without registration: {:?}", msg.id);
255 stats.unknown += 1;
256 }
257 },
258 }
259 }
260
261 #[must_use]
265 pub fn take_watermarks(&mut self) -> Vec<(PartitionId, i64)> {
266 let mut out: Vec<_> = self
267 .trackers
268 .iter_mut()
269 .filter_map(|(&p, t)| t.advance().map(|w| (p, w)))
270 .collect();
271 out.sort_unstable_by_key(|&(p, _)| p);
272 out
273 }
274
275 #[must_use]
277 pub fn pending(&self, partition: PartitionId) -> usize {
278 self.trackers
279 .get(&partition)
280 .map_or(0, PartitionTracker::pending)
281 }
282
283 #[must_use]
285 pub fn max_pending(&self) -> usize {
286 self.trackers
287 .values()
288 .map(PartitionTracker::pending)
289 .max()
290 .unwrap_or(0)
291 }
292
293 #[must_use]
296 pub fn stalled_partitions(&self) -> Vec<(PartitionId, Instant)> {
297 let mut out: Vec<_> = self
298 .trackers
299 .iter()
300 .filter_map(|(&p, t)| t.stalled_since().map(|since| (p, since)))
301 .collect();
302 out.sort_unstable_by_key(|&(p, _)| p);
303 out
304 }
305}
306
307#[cfg(all(test, not(loom)))]
308mod tests {
309 use super::*;
310
311 const P0: PartitionId = PartitionId(0);
312 const P1: PartitionId = PartitionId(1);
313
314 fn checkpointer(partitions: &[PartitionId]) -> (Checkpointer, AckIssuer) {
315 let mut cp = Checkpointer::new();
316 cp.begin_epoch(partitions, 1);
317 let issuer = cp.handle();
318 (cp, issuer)
319 }
320
321 #[test]
322 fn issue_drain_take_happy_path() {
323 let (mut cp, mut issuer) = checkpointer(&[P0]);
324 drop(issuer.issue(P0, 99));
325 drop(issuer.issue(P0, 199));
326 let stats = cp.drain();
327 assert_eq!(stats.applied, 2);
328 assert_eq!(
329 stats,
330 DrainStats {
331 applied: 2,
332 ..Default::default()
333 }
334 );
335 assert_eq!(cp.take_watermarks(), vec![(P0, 200)]);
336 }
337
338 #[test]
339 fn take_watermarks_is_empty_until_new_progress() {
340 let (mut cp, mut issuer) = checkpointer(&[P0]);
341 drop(issuer.issue(P0, 9));
342 cp.drain();
343 assert_eq!(cp.take_watermarks(), vec![(P0, 10)]);
344 assert_eq!(cp.take_watermarks(), vec![]);
345 drop(issuer.issue(P0, 19));
346 cp.drain();
347 assert_eq!(cp.take_watermarks(), vec![(P0, 20)]);
348 }
349
350 #[test]
351 fn out_of_order_acks_across_partitions() {
352 let (mut cp, mut issuer) = checkpointer(&[P0, P1]);
353 let a0 = issuer.issue(P0, 9);
354 let a1 = issuer.issue(P0, 19);
355 let b0 = issuer.issue(P1, 99);
356 drop(a1);
358 drop(b0);
359 cp.drain();
360 assert_eq!(cp.take_watermarks(), vec![(P1, 100)]);
361 assert_eq!(cp.pending(P0), 2);
362 drop(a0);
363 cp.drain();
364 assert_eq!(cp.take_watermarks(), vec![(P0, 20)]);
365 }
366
367 #[test]
368 fn failed_batch_stalls_partition_and_reports() {
369 let (mut cp, mut issuer) = checkpointer(&[P0]);
370 let bad = issuer.issue(P0, 9);
371 bad.fail();
372 drop(bad);
373 drop(issuer.issue(P0, 19));
374 cp.drain();
375 assert_eq!(cp.take_watermarks(), vec![]);
376 let stalled = cp.stalled_partitions();
377 assert_eq!(stalled.len(), 1);
378 assert_eq!(stalled[0].0, P0);
379 }
380
381 #[test]
382 fn stale_epoch_acks_are_discarded() {
383 let (mut cp, mut issuer) = checkpointer(&[P0]);
384 let old = issuer.issue(P0, 9);
385 cp.begin_epoch(&[P0], 2);
386 drop(old); let stats = cp.drain();
388 assert_eq!(stats.applied, 0);
389 assert_eq!(stats.stale_epoch, 2);
391 assert_eq!(cp.take_watermarks(), vec![]);
392
393 drop(issuer.issue(P0, 49));
395 let stats = cp.drain();
396 assert_eq!(stats.applied, 1);
397 assert_eq!(cp.take_watermarks(), vec![(P0, 50)]);
398 }
399
400 #[test]
401 fn revoke_mid_flight_discards_later_acks() {
402 let (mut cp, mut issuer) = checkpointer(&[P0, P1]);
403 let in_flight = issuer.issue(P1, 9);
404 cp.drain(); cp.revoke(&[P1]);
406 drop(in_flight);
407 let stats = cp.drain();
408 assert_eq!(stats.stale_epoch, 1);
409 assert_eq!(cp.take_watermarks(), vec![]);
410 assert_eq!(cp.pending(P1), 0);
411 }
412
413 #[test]
414 fn registration_and_ack_in_same_drain() {
415 let (mut cp, mut issuer) = checkpointer(&[P0]);
418 drop(issuer.issue(P0, 9));
419 let stats = cp.drain();
420 assert_eq!(stats.applied, 1);
421 assert_eq!(stats.unknown, 0);
422 assert_eq!(cp.take_watermarks(), vec![(P0, 10)]);
423 }
424
425 #[test]
426 fn cross_thread_issue_and_resolve() {
427 let (mut cp, issuer) = checkpointer(&[P0, P1]);
428 let handles: Vec<_> = [P0, P1]
429 .into_iter()
430 .map(|p| {
431 let mut issuer = issuer.clone();
432 std::thread::spawn(move || {
433 for i in 0..100i64 {
434 drop(issuer.issue(p, (i + 1) * 10 - 1));
435 }
436 })
437 })
438 .collect();
439 for h in handles {
440 h.join().unwrap();
441 }
442 let stats = cp.drain();
443 assert_eq!(stats.applied, 200);
444 assert_eq!(stats.unknown, 0);
445 assert_eq!(cp.take_watermarks(), vec![(P0, 1000), (P1, 1000)]);
446 }
447
448 #[test]
449 fn pending_counts_feed_backpressure() {
450 let (mut cp, mut issuer) = checkpointer(&[P0, P1]);
451 let held: Vec<_> = (0..5).map(|i| issuer.issue(P0, i)).collect();
452 drop(issuer.issue(P1, 9));
453 cp.drain();
454 assert_eq!(cp.pending(P0), 5);
455 assert_eq!(cp.max_pending(), 5);
456 drop(held);
457 cp.drain();
458 let _ = cp.take_watermarks();
459 assert_eq!(cp.max_pending(), 0);
460 }
461
462 #[test]
463 #[should_panic(expected = "strictly increasing")]
464 fn epoch_regression_panics() {
465 let mut cp = Checkpointer::new();
466 cp.begin_epoch(&[P0], 5);
467 cp.begin_epoch(&[P0], 5);
468 }
469}
470
471#[cfg(all(test, not(loom)))]
472mod proptests {
473 use super::*;
474 use proptest::prelude::*;
475
476 #[derive(Clone, Debug)]
477 enum Op {
478 Issue { partition: u8, fail: bool },
479 ResolveOldest,
480 Rebalance { partitions: Vec<u8> },
481 DrainAndTake,
482 }
483
484 fn ops() -> impl Strategy<Value = Vec<Op>> {
485 prop::collection::vec(
486 prop_oneof![
487 (0..3u8, any::<bool>()).prop_map(|(partition, fail)| Op::Issue { partition, fail }),
488 Just(Op::ResolveOldest),
489 prop::collection::vec(0..3u8, 1..3)
490 .prop_map(|partitions| Op::Rebalance { partitions }),
491 Just(Op::DrainAndTake),
492 ],
493 0..120,
494 )
495 }
496
497 proptest! {
498 #[test]
502 fn epoch_churn_never_leaks_stale_acks(ops in ops()) {
503 let mut cp = Checkpointer::new();
504 let mut epoch = 1u32;
505 let mut assigned: Vec<PartitionId> = vec![PartitionId(0), PartitionId(1), PartitionId(2)];
506 cp.begin_epoch(&assigned, epoch);
507 let mut issuer = cp.handle();
508 let mut offsets: std::collections::HashMap<PartitionId, i64> =
509 std::collections::HashMap::new();
510 let mut held: std::collections::VecDeque<(AckRef, u32, bool)> =
512 std::collections::VecDeque::new();
513 let mut last_watermark: std::collections::HashMap<PartitionId, i64> =
514 std::collections::HashMap::new();
515
516 for op in ops {
517 match op {
518 Op::Issue { partition, fail } => {
519 let p = PartitionId(u32::from(partition));
520 if !assigned.contains(&p) {
521 continue;
522 }
523 let next = offsets.entry(p).or_insert(0);
524 *next += 10;
525 let ack = issuer.issue(p, *next - 1);
526 if fail {
527 ack.fail();
528 }
529 held.push_back((ack, epoch, fail));
530 }
531 Op::ResolveOldest => {
532 held.pop_front(); }
534 Op::Rebalance { partitions } => {
535 epoch += 1;
536 assigned = partitions
537 .into_iter()
538 .map(|p| PartitionId(u32::from(p)))
539 .collect::<std::collections::BTreeSet<_>>()
540 .into_iter()
541 .collect();
542 cp.begin_epoch(&assigned, epoch);
543 offsets.clear();
546 last_watermark.clear();
547 }
548 Op::DrainAndTake => {
549 cp.drain();
550 for (p, w) in cp.take_watermarks() {
551 prop_assert!(
552 assigned.contains(&p),
553 "watermark for unassigned partition {p:?}"
554 );
555 if let Some(&prev) = last_watermark.get(&p) {
556 prop_assert!(w > prev, "watermark not monotonic for {p:?}");
557 }
558 last_watermark.insert(p, w);
559 }
560 }
561 }
562 }
563
564 let stale_epochs: Vec<u32> =
567 held.iter().map(|&(_, e, _)| e).filter(|&e| e != epoch).collect();
568 held.clear();
569 let stats = cp.drain();
570 prop_assert!(stats.unknown == 0, "driver-bug resolutions: {stats:?}");
571 for (p, w) in cp.take_watermarks() {
572 prop_assert!(assigned.contains(&p));
573 if let Some(&prev) = last_watermark.get(&p) {
574 prop_assert!(w > prev);
575 }
576 }
577 if !stale_epochs.is_empty() {
579 prop_assert!(stats.stale_epoch > 0);
580 }
581 }
582 }
583}