1use crate::{Epoch, LOGICAL_MAX, PHYSICAL_MS_MAX, Timestamp};
17
18#[derive(Copy, Clone, Debug, PartialEq, Eq)]
19pub struct WindowGrant {
20 pub physical_ms: u64,
21 pub logical_start: u32,
22 pub count: u32,
23 pub epoch: Epoch,
24}
25
26impl WindowGrant {
27 pub fn first(&self) -> Timestamp {
28 Timestamp::pack(self.physical_ms, self.logical_start)
29 }
30 pub fn last(&self) -> Timestamp {
31 Timestamp::pack(self.physical_ms, self.logical_start + self.count - 1)
32 }
33}
34
35#[derive(Debug, thiserror::Error, PartialEq, Eq)]
36pub enum CoreError {
37 #[error("not leader")]
38 NotLeader,
39 #[error("window exhausted; caller must extend before retrying")]
40 WindowExhausted,
41 #[error("invalid count: {0}")]
42 InvalidCount(u32),
43 #[error("physical_ms {0} exceeds 46-bit maximum")]
44 PhysicalMsOutOfRange(u64),
45 #[error(
46 "invalid leadership window: fence_floor {fence_floor} exceeds committed_ceiling {committed_ceiling}"
47 )]
48 InvalidLeadershipWindow {
49 fence_floor: u64,
50 committed_ceiling: u64,
51 },
52}
53
54#[derive(Debug)]
55enum State {
56 NotLeader,
57 Leader {
58 epoch: Epoch,
59 committed_high_water: u64,
62 next_physical_ms: u64,
66 next_logical: u32,
68 },
69}
70
71pub struct Allocator {
72 state: State,
73}
74
75impl Allocator {
76 pub fn new() -> Self {
77 Allocator {
78 state: State::NotLeader,
79 }
80 }
81
82 pub fn try_on_leadership_gained(
94 &mut self,
95 fence_floor: u64,
96 committed_ceiling: u64,
97 epoch: Epoch,
98 ) -> Result<(), CoreError> {
99 if fence_floor > PHYSICAL_MS_MAX {
100 return Err(CoreError::PhysicalMsOutOfRange(fence_floor));
101 }
102 if committed_ceiling > PHYSICAL_MS_MAX {
103 return Err(CoreError::PhysicalMsOutOfRange(committed_ceiling));
104 }
105 if committed_ceiling < fence_floor {
106 return Err(CoreError::InvalidLeadershipWindow {
107 fence_floor,
108 committed_ceiling,
109 });
110 }
111 self.state = State::Leader {
112 epoch,
113 committed_high_water: committed_ceiling,
114 next_physical_ms: fence_floor,
115 next_logical: 0,
116 };
117 Ok(())
118 }
119
120 pub fn on_leadership_lost(&mut self) {
121 self.state = State::NotLeader;
122 }
123
124 pub fn is_leader(&self) -> bool {
125 matches!(self.state, State::Leader { .. })
126 }
127
128 pub fn epoch(&self) -> Option<Epoch> {
129 match self.state {
130 State::Leader { epoch, .. } => Some(epoch),
131 State::NotLeader => None,
132 }
133 }
134
135 pub fn try_grant(&mut self, now_ms: u64, count: u32) -> Result<WindowGrant, CoreError> {
140 if count == 0 {
141 return Err(CoreError::InvalidCount(0));
142 }
143 if count > LOGICAL_MAX + 1 {
144 return Err(CoreError::InvalidCount(count));
145 }
146 let State::Leader {
147 epoch,
148 committed_high_water,
149 next_physical_ms,
150 next_logical,
151 } = &mut self.state
152 else {
153 return Err(CoreError::NotLeader);
154 };
155
156 if now_ms > *next_physical_ms {
159 *next_physical_ms = now_ms;
160 *next_logical = 0;
161 }
162
163 if *next_logical as u64 + count as u64 > LOGICAL_MAX as u64 + 1 {
166 *next_physical_ms += 1;
167 *next_logical = 0;
168 }
169
170 if *next_physical_ms > PHYSICAL_MS_MAX {
171 return Err(CoreError::PhysicalMsOutOfRange(*next_physical_ms));
172 }
173
174 if *next_physical_ms > *committed_high_water {
177 return Err(CoreError::WindowExhausted);
178 }
179
180 let grant = WindowGrant {
181 physical_ms: *next_physical_ms,
182 logical_start: *next_logical,
183 count,
184 epoch: *epoch,
185 };
186 *next_logical += count;
187 Ok(grant)
188 }
189
190 pub fn would_grant(&self, now_ms: u64, count: u32) -> bool {
198 if count == 0 || count > LOGICAL_MAX + 1 {
199 return false;
200 }
201 let State::Leader {
202 committed_high_water,
203 next_physical_ms,
204 next_logical,
205 ..
206 } = &self.state
207 else {
208 return false;
209 };
210
211 let mut physical_ms = *next_physical_ms;
212 let mut logical = *next_logical;
213 if now_ms > physical_ms {
214 physical_ms = now_ms;
215 logical = 0;
216 }
217 if logical as u64 + count as u64 > LOGICAL_MAX as u64 + 1 {
218 physical_ms += 1;
219 }
220 if physical_ms > PHYSICAL_MS_MAX {
221 return false;
222 }
223 physical_ms <= *committed_high_water
224 }
225
226 pub fn try_prepare_window_extension(
238 &self,
239 now_ms: u64,
240 ahead_ms: u64,
241 ) -> Result<u64, CoreError> {
242 match &self.state {
243 State::NotLeader => Err(CoreError::NotLeader),
244 State::Leader {
245 committed_high_water,
246 ..
247 } => {
248 let floor = committed_high_water
249 .checked_add(1)
250 .ok_or(CoreError::PhysicalMsOutOfRange(*committed_high_water))?;
251 let requested = core::cmp::max(floor, now_ms)
252 .checked_add(ahead_ms)
253 .ok_or(CoreError::PhysicalMsOutOfRange(u64::MAX))?;
254 if requested > PHYSICAL_MS_MAX {
255 return Err(CoreError::PhysicalMsOutOfRange(requested));
256 }
257 Ok(requested)
258 }
259 }
260 }
261
262 pub fn try_commit_window_extension(
273 &mut self,
274 persisted_high_water: u64,
275 expected_epoch: Epoch,
276 ) -> Result<(), CoreError> {
277 if persisted_high_water > PHYSICAL_MS_MAX {
278 return Err(CoreError::PhysicalMsOutOfRange(persisted_high_water));
279 }
280 if let State::Leader {
281 epoch,
282 committed_high_water,
283 ..
284 } = &mut self.state
285 && *epoch == expected_epoch
286 && persisted_high_water > *committed_high_water
287 {
288 *committed_high_water = persisted_high_water;
289 }
290 Ok(())
291 }
292}
293
294impl Default for Allocator {
295 fn default() -> Self {
296 Self::new()
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 #[test]
305 fn new_allocator_is_not_leader() {
306 let allocator = Allocator::new();
307 assert!(!allocator.is_leader());
308 assert_eq!(allocator.epoch(), None);
309 }
310
311 #[test]
312 fn on_leadership_gained_sets_epoch() {
313 let mut allocator = Allocator::new();
314 allocator
315 .try_on_leadership_gained(1000, 5000, Epoch(5))
316 .unwrap();
317 assert!(allocator.is_leader());
318 assert_eq!(allocator.epoch(), Some(Epoch(5)));
319 }
320
321 #[test]
322 fn try_on_leadership_gained_rejects_out_of_range_window() {
323 let mut allocator = Allocator::new();
324 assert_eq!(
325 allocator.try_on_leadership_gained(PHYSICAL_MS_MAX + 1, PHYSICAL_MS_MAX + 1, Epoch(5)),
326 Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1))
327 );
328 assert_eq!(
330 allocator.try_on_leadership_gained(1_000, PHYSICAL_MS_MAX + 1, Epoch(5)),
331 Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1))
332 );
333 assert_eq!(
334 allocator.try_on_leadership_gained(5000, 4000, Epoch(5)),
335 Err(CoreError::InvalidLeadershipWindow {
336 fence_floor: 5000,
337 committed_ceiling: 4000
338 })
339 );
340 }
341
342 #[test]
343 fn on_leadership_lost_clears_state() {
344 let mut allocator = Allocator::new();
345 allocator
346 .try_on_leadership_gained(1000, 5000, Epoch(5))
347 .unwrap();
348 allocator.on_leadership_lost();
349 assert!(!allocator.is_leader());
350 assert_eq!(allocator.epoch(), None);
351 }
352
353 #[test]
354 fn try_grant_not_leader() {
355 let mut allocator = Allocator::new();
356 assert_eq!(allocator.try_grant(1000, 1), Err(CoreError::NotLeader));
357 }
358
359 #[test]
360 fn try_grant_zero_count() {
361 let mut allocator = Allocator::new();
362 allocator
363 .try_on_leadership_gained(1000, 5000, Epoch(1))
364 .unwrap();
365 assert_eq!(
366 allocator.try_grant(1000, 0),
367 Err(CoreError::InvalidCount(0))
368 );
369 }
370
371 #[test]
372 fn try_grant_oversized_count() {
373 let mut allocator = Allocator::new();
374 allocator
375 .try_on_leadership_gained(1000, 5000, Epoch(1))
376 .unwrap();
377 let oversized = LOGICAL_MAX + 2;
378 assert_eq!(
379 allocator.try_grant(1000, oversized),
380 Err(CoreError::InvalidCount(oversized))
381 );
382 }
383
384 #[test]
385 fn try_grant_above_committed_is_window_exhausted() {
386 let mut allocator = Allocator::new();
389 allocator
391 .try_on_leadership_gained(5_000, 5_000, Epoch(1))
392 .unwrap();
393 allocator.try_grant(4_999, 1).unwrap();
395 assert_eq!(
397 allocator.try_grant(5_001, 1),
398 Err(CoreError::WindowExhausted)
399 );
400 }
401
402 #[test]
403 fn try_grant_after_gain_serves_immediately() {
404 let mut allocator = Allocator::new();
407 allocator
408 .try_on_leadership_gained(5_000, 10_000, Epoch(1))
409 .unwrap();
410 let grant = allocator.try_grant(1_000, 1).unwrap();
411 assert_eq!(grant.physical_ms, 5_000);
413 assert_eq!(grant.logical_start, 0);
414 assert_eq!(grant.epoch, Epoch(1));
415 }
416
417 #[test]
418 fn prepare_window_extension_not_leader() {
419 let allocator = Allocator::new();
422 assert_eq!(
423 allocator.try_prepare_window_extension(1000, 3000),
424 Err(CoreError::NotLeader)
425 );
426 }
427
428 #[test]
429 fn prepare_window_extension_uses_now_ms_when_ahead_of_high_water() {
430 let mut allocator = Allocator::new();
431 allocator
432 .try_on_leadership_gained(1000, 1000, Epoch(1))
433 .unwrap();
434 let target = allocator.try_prepare_window_extension(2000, 3000).unwrap();
435 assert_eq!(target, 5000); }
437
438 #[test]
439 fn prepare_window_extension_uses_high_water_floor_when_clock_behind() {
440 let mut allocator = Allocator::new();
441 allocator
442 .try_on_leadership_gained(10_000, 10_000, Epoch(1))
443 .unwrap();
444 let target = allocator.try_prepare_window_extension(500, 3000).unwrap();
445 assert_eq!(target, 13_001);
447 }
448
449 #[test]
450 fn prepare_window_extension_rejects_out_of_range_target() {
451 let mut allocator = Allocator::new();
452 allocator
453 .try_on_leadership_gained(PHYSICAL_MS_MAX, PHYSICAL_MS_MAX, Epoch(1))
454 .unwrap();
455 assert_eq!(
456 allocator.try_prepare_window_extension(PHYSICAL_MS_MAX, 1),
457 Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 2))
458 );
459 }
460
461 #[test]
462 fn commit_then_try_grant_succeeds() {
463 let mut allocator = Allocator::new();
464 allocator
465 .try_on_leadership_gained(1000, 1000, Epoch(7))
466 .unwrap();
467 let target = allocator.try_prepare_window_extension(1000, 3000).unwrap();
468 allocator
469 .try_commit_window_extension(target, Epoch(7))
470 .unwrap();
471 let grant = allocator.try_grant(1000, 5).unwrap();
472 assert_eq!(grant.count, 5);
473 assert_eq!(grant.logical_start, 0);
474 assert_eq!(grant.epoch, Epoch(7));
475 assert!(grant.physical_ms <= target);
477 }
478
479 #[test]
480 fn commit_with_lower_value_is_ignored() {
481 let mut allocator = Allocator::new();
482 allocator
483 .try_on_leadership_gained(1000, 1000, Epoch(1))
484 .unwrap();
485 allocator
486 .try_commit_window_extension(5000, Epoch(1))
487 .unwrap();
488 allocator
489 .try_commit_window_extension(3000, Epoch(1))
490 .unwrap(); let grant = allocator.try_grant(4500, 1).unwrap();
493 assert_eq!(grant.physical_ms, 4500);
494 }
495
496 #[test]
497 fn commit_rejects_out_of_range_high_water() {
498 let mut allocator = Allocator::new();
499 allocator
500 .try_on_leadership_gained(1000, 1000, Epoch(1))
501 .unwrap();
502 assert_eq!(
503 allocator.try_commit_window_extension(PHYSICAL_MS_MAX + 1, Epoch(1)),
504 Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1))
505 );
506 }
507
508 #[test]
509 fn try_grant_rejects_out_of_range_clock() {
510 let mut allocator = Allocator::new();
511 allocator
512 .try_on_leadership_gained(1000, PHYSICAL_MS_MAX, Epoch(1))
513 .unwrap();
514 assert_eq!(
515 allocator.try_grant(PHYSICAL_MS_MAX + 1, 1),
516 Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1))
517 );
518 }
519
520 #[test]
521 fn commit_at_wrong_epoch_is_silently_dropped() {
522 let mut allocator = Allocator::new();
523 allocator
525 .try_on_leadership_gained(1000, 1000, Epoch(5))
526 .unwrap();
527 allocator
529 .try_commit_window_extension(9_999, Epoch(4))
530 .unwrap();
531 allocator.try_grant(900, 1).unwrap();
534 assert_eq!(
535 allocator.try_grant(1_100, 1),
536 Err(CoreError::WindowExhausted)
537 );
538 }
539
540 #[test]
541 fn commit_after_leadership_lost_is_ignored() {
542 let mut allocator = Allocator::new();
543 allocator
544 .try_on_leadership_gained(1000, 5000, Epoch(1))
545 .unwrap();
546 allocator.on_leadership_lost();
547 allocator
548 .try_commit_window_extension(9_999, Epoch(1))
549 .unwrap();
550 assert!(!allocator.is_leader());
551 }
552
553 #[test]
554 fn would_grant_matches_try_grant_outcome() {
555 let mut allocator = Allocator::new();
556 assert!(!allocator.would_grant(1_000, 1));
558 allocator
560 .try_on_leadership_gained(1_000, 5_000, Epoch(1))
561 .unwrap();
562 assert!(!allocator.would_grant(1_000, 0));
563 assert!(!allocator.would_grant(1_000, LOGICAL_MAX + 2));
564 assert!(allocator.would_grant(0, 1));
567 assert!(!allocator.would_grant(5_001, 1));
569 assert!(allocator.would_grant(2_500, 1));
571 }
572
573 #[test]
574 fn would_grant_predicts_logical_wrap_advance() {
575 let mut allocator = Allocator::new();
579 allocator
580 .try_on_leadership_gained(1_000, 1_000, Epoch(1))
581 .unwrap();
582 allocator.try_grant(1_000, LOGICAL_MAX + 1).unwrap();
587 assert!(!allocator.would_grant(1_000, 1));
590 }
591
592 #[test]
593 fn would_grant_returns_false_when_advance_exceeds_physical_max() {
594 let mut allocator = Allocator::new();
597 allocator
598 .try_on_leadership_gained(PHYSICAL_MS_MAX, PHYSICAL_MS_MAX, Epoch(1))
599 .unwrap();
600 allocator
603 .try_grant(PHYSICAL_MS_MAX, LOGICAL_MAX + 1)
604 .unwrap();
605 assert!(!allocator.would_grant(PHYSICAL_MS_MAX, 1));
606 }
607
608 #[test]
609 fn default_constructs_not_leader_allocator() {
610 let allocator = Allocator::default();
611 assert!(!allocator.is_leader());
612 assert_eq!(allocator.epoch(), None);
613 }
614
615 #[test]
616 fn logical_wraps_to_next_physical_ms() {
617 let mut allocator = Allocator::new();
618 allocator.try_on_leadership_gained(0, 0, Epoch(1)).unwrap();
620 allocator.try_commit_window_extension(10, Epoch(1)).unwrap();
621 let first = allocator.try_grant(1, LOGICAL_MAX + 1).unwrap();
623 assert_eq!(first.physical_ms, 1);
624 assert_eq!(first.logical_start, 0);
625 let second = allocator.try_grant(1, 1).unwrap();
626 assert_eq!(second.physical_ms, 2);
627 assert_eq!(second.logical_start, 0);
628 }
629}