1use super::AckStatus;
15use crate::clock::{Clock, SystemClock};
16use std::collections::VecDeque;
17use std::time::Instant;
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21#[must_use]
22pub enum ResolveOutcome {
23 Applied,
25 Duplicate,
27 AlreadyAdvanced,
30 Unregistered,
34}
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37enum SlotState {
38 Pending,
39 Delivered,
40 Failed,
41}
42
43#[derive(Clone, Copy, Debug)]
44struct Slot {
45 last_offset: i64,
46 state: SlotState,
47}
48
49#[derive(Debug, Default)]
67pub struct PartitionTracker {
68 head_seq: u64,
70 next_seq: u64,
72 ring: VecDeque<Slot>,
73 stalled_since: Option<Instant>,
75 stalled_seq: Option<u64>,
77}
78
79impl PartitionTracker {
80 #[must_use]
82 pub fn new() -> Self {
83 Self::default()
84 }
85
86 pub fn register(&mut self, seq: u64, last_offset: i64) {
97 assert_eq!(
98 seq, self.next_seq,
99 "batch registration gap: expected seq {}, got {seq}",
100 self.next_seq
101 );
102 self.ring.push_back(Slot {
103 last_offset,
104 state: SlotState::Pending,
105 });
106 self.next_seq += 1;
107 }
108
109 pub fn resolve(&mut self, seq: u64, status: AckStatus) -> ResolveOutcome {
113 if seq < self.head_seq {
114 debug_assert!(false, "resolution for already-advanced batch {seq}");
115 return ResolveOutcome::AlreadyAdvanced;
116 }
117 let Ok(idx) = usize::try_from(seq - self.head_seq) else {
118 return ResolveOutcome::Unregistered;
119 };
120 let Some(slot) = self.ring.get_mut(idx) else {
121 return ResolveOutcome::Unregistered;
122 };
123 if slot.state != SlotState::Pending {
124 debug_assert!(false, "duplicate resolution for batch {seq}");
125 return ResolveOutcome::Duplicate;
126 }
127 slot.state = match status {
128 AckStatus::Delivered => SlotState::Delivered,
129 AckStatus::Failed => SlotState::Failed,
130 };
131 ResolveOutcome::Applied
132 }
133
134 pub fn advance(&mut self) -> Option<i64> {
142 self.advance_on(&SystemClock)
143 }
144
145 pub(crate) fn advance_on(&mut self, clock: &dyn Clock) -> Option<i64> {
147 let mut committable = None;
148 while let Some(slot) = self.ring.front() {
149 match slot.state {
150 SlotState::Delivered => {
151 committable = Some(slot.last_offset + 1);
152 self.ring.pop_front();
153 self.head_seq += 1;
154 }
155 SlotState::Failed => {
156 if self.stalled_since.is_none() {
157 self.stalled_since = Some(clock.now());
158 self.stalled_seq = Some(self.head_seq);
159 }
160 break;
161 }
162 SlotState::Pending => break,
163 }
164 }
165 committable
166 }
167
168 #[must_use]
171 pub fn stalled(&self) -> bool {
172 self.stalled_since.is_some()
173 }
174
175 #[must_use]
177 pub fn stalled_since(&self) -> Option<Instant> {
178 self.stalled_since
179 }
180
181 #[must_use]
183 pub fn stalled_seq(&self) -> Option<u64> {
184 self.stalled_seq
185 }
186
187 #[must_use]
190 pub fn pending(&self) -> usize {
191 self.ring.len()
192 }
193}
194
195#[cfg(all(test, not(loom)))]
196mod tests {
197 use super::*;
198
199 fn delivered(t: &mut PartitionTracker, seq: u64) {
200 assert_eq!(
201 t.resolve(seq, AckStatus::Delivered),
202 ResolveOutcome::Applied
203 );
204 }
205
206 fn failed(t: &mut PartitionTracker, seq: u64) {
207 assert_eq!(t.resolve(seq, AckStatus::Failed), ResolveOutcome::Applied);
208 }
209
210 #[test]
211 fn happy_path_in_order() {
212 let mut t = PartitionTracker::new();
213 t.register(0, 9);
214 t.register(1, 19);
215 t.register(2, 29);
216 delivered(&mut t, 0);
217 assert_eq!(t.advance(), Some(10));
218 delivered(&mut t, 1);
219 delivered(&mut t, 2);
220 assert_eq!(t.advance(), Some(30));
221 assert_eq!(t.advance(), None);
222 assert_eq!(t.pending(), 0);
223 }
224
225 #[test]
226 fn single_batch() {
227 let mut t = PartitionTracker::new();
228 t.register(0, 41);
229 assert_eq!(t.advance(), None);
230 delivered(&mut t, 0);
231 assert_eq!(t.advance(), Some(42));
232 }
233
234 #[test]
235 fn out_of_order_resolution_waits_for_head() {
236 let mut t = PartitionTracker::new();
237 for seq in 0..4 {
238 t.register(seq, (seq as i64 + 1) * 10 - 1);
239 }
240 delivered(&mut t, 2);
241 delivered(&mut t, 3);
242 assert_eq!(t.advance(), None);
243 delivered(&mut t, 0);
244 assert_eq!(t.advance(), Some(10));
245 delivered(&mut t, 1);
246 assert_eq!(t.advance(), Some(40));
247 assert_eq!(t.pending(), 0);
248 }
249
250 #[test]
251 fn failure_at_head_stalls_permanently() {
252 let mut t = PartitionTracker::new();
253 t.register(0, 9);
254 t.register(1, 19);
255 failed(&mut t, 0);
256 delivered(&mut t, 1);
257 assert_eq!(t.advance(), None);
258 assert!(t.stalled());
259 assert_eq!(t.stalled_seq(), Some(0));
260 assert!(t.stalled_since().is_some());
261 t.register(2, 29);
263 delivered(&mut t, 2);
264 assert_eq!(t.advance(), None);
265 assert_eq!(t.pending(), 3);
266 }
267
268 #[test]
269 fn advance_commits_up_to_failure() {
270 let mut t = PartitionTracker::new();
271 t.register(0, 9);
272 t.register(1, 19);
273 t.register(2, 29);
274 delivered(&mut t, 0);
275 failed(&mut t, 1);
276 delivered(&mut t, 2);
277 assert_eq!(t.advance(), Some(10));
279 assert!(t.stalled());
280 assert_eq!(t.stalled_seq(), Some(1));
281 }
282
283 #[test]
284 fn failure_behind_pending_stalls_only_when_reached() {
285 let mut t = PartitionTracker::new();
286 t.register(0, 9);
287 t.register(1, 19);
288 failed(&mut t, 1);
289 assert_eq!(t.advance(), None);
290 assert!(!t.stalled(), "failure not yet at head");
291 delivered(&mut t, 0);
292 assert_eq!(t.advance(), Some(10));
293 assert!(t.stalled());
294 }
295
296 #[test]
297 fn duplicate_and_stale_resolutions_are_reported() {
298 let mut t = PartitionTracker::new();
299 t.register(0, 9);
300 t.register(1, 19);
301 delivered(&mut t, 0);
302 assert_eq!(t.advance(), Some(10));
303
304 assert_eq!(
306 t.resolve(5, AckStatus::Delivered),
307 ResolveOutcome::Unregistered
308 );
309
310 if cfg!(not(debug_assertions)) {
313 assert_eq!(
314 t.resolve(0, AckStatus::Delivered),
315 ResolveOutcome::AlreadyAdvanced
316 );
317 delivered(&mut t, 1);
318 assert_eq!(
319 t.resolve(1, AckStatus::Delivered),
320 ResolveOutcome::Duplicate
321 );
322 }
323 }
324
325 #[test]
326 #[should_panic(expected = "batch registration gap")]
327 fn registration_gap_panics() {
328 let mut t = PartitionTracker::new();
329 t.register(0, 9);
330 t.register(2, 29);
331 }
332
333 #[test]
334 fn watermark_is_monotonic() {
335 let mut t = PartitionTracker::new();
336 let mut last = 0;
337 for seq in 0..100u64 {
338 t.register(seq, (seq as i64 + 1) * 7);
339 }
340 for step in 0..100u64 {
342 let seq = (step * 37) % 100;
343 delivered(&mut t, seq);
344 if let Some(w) = t.advance() {
345 assert!(w > last, "watermark went backwards: {last} -> {w}");
346 last = w;
347 }
348 }
349 assert_eq!(last, 100 * 7 + 1);
350 assert_eq!(t.pending(), 0);
351 }
352}
353
354#[cfg(all(test, not(loom)))]
355mod proptests {
356 use super::*;
357 use proptest::prelude::*;
358 use std::collections::BTreeMap;
359
360 #[derive(Clone, Debug)]
361 enum Op {
362 Register {
363 last_offset_step: i64,
364 },
365 Resolve {
368 pick: usize,
369 fail: bool,
370 },
371 Advance,
372 }
373
374 fn ops() -> impl Strategy<Value = Vec<Op>> {
375 prop::collection::vec(
376 prop_oneof![
377 (1..100i64).prop_map(|last_offset_step| Op::Register { last_offset_step }),
378 (any::<usize>(), any::<bool>()).prop_map(|(pick, fail)| Op::Resolve { pick, fail }),
379 Just(Op::Advance),
380 ],
381 0..200,
382 )
383 }
384
385 #[derive(Default)]
388 struct Reference {
389 batches: BTreeMap<u64, (i64, Option<AckStatus>)>,
390 committed: u64,
391 next_seq: u64,
392 next_offset: i64,
393 stalled: bool,
394 }
395
396 impl Reference {
397 fn expected_advance(&mut self) -> Option<i64> {
400 let mut committable = None;
401 while let Some(&(last_offset, status)) = self.batches.get(&self.committed) {
402 match status {
403 Some(AckStatus::Delivered) => {
404 committable = Some(last_offset + 1);
405 self.batches.remove(&self.committed);
406 self.committed += 1;
407 }
408 Some(AckStatus::Failed) => {
409 self.stalled = true;
410 break;
411 }
412 None => break,
413 }
414 }
415 committable
416 }
417 }
418
419 proptest! {
420 #[test]
421 fn tracker_matches_reference_model(ops in ops()) {
422 let mut tracker = PartitionTracker::new();
423 let mut reference = Reference::default();
424 let mut unresolved: Vec<u64> = Vec::new();
425 let mut watermark = None;
426
427 for op in ops {
428 match op {
429 Op::Register { last_offset_step } => {
430 reference.next_offset += last_offset_step;
431 let seq = reference.next_seq;
432 reference.next_seq += 1;
433 reference.batches.insert(seq, (reference.next_offset, None));
434 tracker.register(seq, reference.next_offset);
435 unresolved.push(seq);
436 }
437 Op::Resolve { pick, fail } => {
438 if unresolved.is_empty() {
439 continue;
440 }
441 let seq = unresolved.swap_remove(pick % unresolved.len());
442 let status = if fail { AckStatus::Failed } else { AckStatus::Delivered };
443 reference.batches.get_mut(&seq).unwrap().1 = Some(status);
444 prop_assert_eq!(tracker.resolve(seq, status), ResolveOutcome::Applied);
445 }
446 Op::Advance => {
447 let got = tracker.advance();
448 let expected = reference.expected_advance();
449 prop_assert_eq!(got, expected);
450 if let Some(w) = got {
451 if let Some(prev) = watermark {
452 prop_assert!(w > prev, "watermark not monotonic");
453 }
454 watermark = Some(got.unwrap());
455 }
456 prop_assert_eq!(tracker.stalled(), reference.stalled);
457 }
458 }
459 prop_assert_eq!(
460 tracker.pending(),
461 reference.batches.len(),
462 "pending mismatch"
463 );
464 }
465
466 let got = tracker.advance();
468 let expected = reference.expected_advance();
469 prop_assert_eq!(got, expected);
470 prop_assert_eq!(tracker.stalled(), reference.stalled);
471 }
472 }
473}