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