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(
233 &self,
234 now_ms: u64,
235 ahead_ms: u64,
236 ) -> Result<u64, CoreError> {
237 match &self.state {
238 State::NotLeader => Ok(0),
239 State::Leader {
240 committed_high_water,
241 ..
242 } => {
243 let floor = committed_high_water
244 .checked_add(1)
245 .ok_or(CoreError::PhysicalMsOutOfRange(*committed_high_water))?;
246 let requested = core::cmp::max(floor, now_ms)
247 .checked_add(ahead_ms)
248 .ok_or(CoreError::PhysicalMsOutOfRange(u64::MAX))?;
249 if requested > PHYSICAL_MS_MAX {
250 return Err(CoreError::PhysicalMsOutOfRange(requested));
251 }
252 Ok(requested)
253 }
254 }
255 }
256
257 pub fn try_commit_window_extension(
268 &mut self,
269 persisted_high_water: u64,
270 expected_epoch: Epoch,
271 ) -> Result<(), CoreError> {
272 if persisted_high_water > PHYSICAL_MS_MAX {
273 return Err(CoreError::PhysicalMsOutOfRange(persisted_high_water));
274 }
275 if let State::Leader {
276 epoch,
277 committed_high_water,
278 ..
279 } = &mut self.state
280 && *epoch == expected_epoch
281 && persisted_high_water > *committed_high_water
282 {
283 *committed_high_water = persisted_high_water;
284 }
285 Ok(())
286 }
287}
288
289impl Default for Allocator {
290 fn default() -> Self {
291 Self::new()
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
300 fn new_allocator_is_not_leader() {
301 let allocator = Allocator::new();
302 assert!(!allocator.is_leader());
303 assert_eq!(allocator.epoch(), None);
304 }
305
306 #[test]
307 fn on_leadership_gained_sets_epoch() {
308 let mut allocator = Allocator::new();
309 allocator
310 .try_on_leadership_gained(1000, 5000, Epoch(5))
311 .unwrap();
312 assert!(allocator.is_leader());
313 assert_eq!(allocator.epoch(), Some(Epoch(5)));
314 }
315
316 #[test]
317 fn try_on_leadership_gained_rejects_out_of_range_window() {
318 let mut allocator = Allocator::new();
319 assert_eq!(
320 allocator.try_on_leadership_gained(PHYSICAL_MS_MAX + 1, PHYSICAL_MS_MAX + 1, Epoch(5)),
321 Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1))
322 );
323 assert_eq!(
325 allocator.try_on_leadership_gained(1_000, PHYSICAL_MS_MAX + 1, Epoch(5)),
326 Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1))
327 );
328 assert_eq!(
329 allocator.try_on_leadership_gained(5000, 4000, Epoch(5)),
330 Err(CoreError::InvalidLeadershipWindow {
331 fence_floor: 5000,
332 committed_ceiling: 4000
333 })
334 );
335 }
336
337 #[test]
338 fn on_leadership_lost_clears_state() {
339 let mut allocator = Allocator::new();
340 allocator
341 .try_on_leadership_gained(1000, 5000, Epoch(5))
342 .unwrap();
343 allocator.on_leadership_lost();
344 assert!(!allocator.is_leader());
345 assert_eq!(allocator.epoch(), None);
346 }
347
348 #[test]
349 fn try_grant_not_leader() {
350 let mut allocator = Allocator::new();
351 assert_eq!(allocator.try_grant(1000, 1), Err(CoreError::NotLeader));
352 }
353
354 #[test]
355 fn try_grant_zero_count() {
356 let mut allocator = Allocator::new();
357 allocator
358 .try_on_leadership_gained(1000, 5000, Epoch(1))
359 .unwrap();
360 assert_eq!(
361 allocator.try_grant(1000, 0),
362 Err(CoreError::InvalidCount(0))
363 );
364 }
365
366 #[test]
367 fn try_grant_oversized_count() {
368 let mut allocator = Allocator::new();
369 allocator
370 .try_on_leadership_gained(1000, 5000, Epoch(1))
371 .unwrap();
372 let oversized = LOGICAL_MAX + 2;
373 assert_eq!(
374 allocator.try_grant(1000, oversized),
375 Err(CoreError::InvalidCount(oversized))
376 );
377 }
378
379 #[test]
380 fn try_grant_above_committed_is_window_exhausted() {
381 let mut allocator = Allocator::new();
384 allocator
386 .try_on_leadership_gained(5_000, 5_000, Epoch(1))
387 .unwrap();
388 allocator.try_grant(4_999, 1).unwrap();
390 assert_eq!(
392 allocator.try_grant(5_001, 1),
393 Err(CoreError::WindowExhausted)
394 );
395 }
396
397 #[test]
398 fn try_grant_after_gain_serves_immediately() {
399 let mut allocator = Allocator::new();
402 allocator
403 .try_on_leadership_gained(5_000, 10_000, Epoch(1))
404 .unwrap();
405 let grant = allocator.try_grant(1_000, 1).unwrap();
406 assert_eq!(grant.physical_ms, 5_000);
408 assert_eq!(grant.logical_start, 0);
409 assert_eq!(grant.epoch, Epoch(1));
410 }
411
412 #[test]
413 fn prepare_window_extension_uses_now_ms_when_ahead_of_high_water() {
414 let mut allocator = Allocator::new();
415 allocator
416 .try_on_leadership_gained(1000, 1000, Epoch(1))
417 .unwrap();
418 let target = allocator.try_prepare_window_extension(2000, 3000).unwrap();
419 assert_eq!(target, 5000); }
421
422 #[test]
423 fn prepare_window_extension_uses_high_water_floor_when_clock_behind() {
424 let mut allocator = Allocator::new();
425 allocator
426 .try_on_leadership_gained(10_000, 10_000, Epoch(1))
427 .unwrap();
428 let target = allocator.try_prepare_window_extension(500, 3000).unwrap();
429 assert_eq!(target, 13_001);
431 }
432
433 #[test]
434 fn prepare_window_extension_rejects_out_of_range_target() {
435 let mut allocator = Allocator::new();
436 allocator
437 .try_on_leadership_gained(PHYSICAL_MS_MAX, PHYSICAL_MS_MAX, Epoch(1))
438 .unwrap();
439 assert_eq!(
440 allocator.try_prepare_window_extension(PHYSICAL_MS_MAX, 1),
441 Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 2))
442 );
443 }
444
445 #[test]
446 fn commit_then_try_grant_succeeds() {
447 let mut allocator = Allocator::new();
448 allocator
449 .try_on_leadership_gained(1000, 1000, Epoch(7))
450 .unwrap();
451 let target = allocator.try_prepare_window_extension(1000, 3000).unwrap();
452 allocator
453 .try_commit_window_extension(target, Epoch(7))
454 .unwrap();
455 let grant = allocator.try_grant(1000, 5).unwrap();
456 assert_eq!(grant.count, 5);
457 assert_eq!(grant.logical_start, 0);
458 assert_eq!(grant.epoch, Epoch(7));
459 assert!(grant.physical_ms <= target);
461 }
462
463 #[test]
464 fn commit_with_lower_value_is_ignored() {
465 let mut allocator = Allocator::new();
466 allocator
467 .try_on_leadership_gained(1000, 1000, Epoch(1))
468 .unwrap();
469 allocator
470 .try_commit_window_extension(5000, Epoch(1))
471 .unwrap();
472 allocator
473 .try_commit_window_extension(3000, Epoch(1))
474 .unwrap(); let grant = allocator.try_grant(4500, 1).unwrap();
477 assert_eq!(grant.physical_ms, 4500);
478 }
479
480 #[test]
481 fn commit_rejects_out_of_range_high_water() {
482 let mut allocator = Allocator::new();
483 allocator
484 .try_on_leadership_gained(1000, 1000, Epoch(1))
485 .unwrap();
486 assert_eq!(
487 allocator.try_commit_window_extension(PHYSICAL_MS_MAX + 1, Epoch(1)),
488 Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1))
489 );
490 }
491
492 #[test]
493 fn try_grant_rejects_out_of_range_clock() {
494 let mut allocator = Allocator::new();
495 allocator
496 .try_on_leadership_gained(1000, PHYSICAL_MS_MAX, Epoch(1))
497 .unwrap();
498 assert_eq!(
499 allocator.try_grant(PHYSICAL_MS_MAX + 1, 1),
500 Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1))
501 );
502 }
503
504 #[test]
505 fn commit_at_wrong_epoch_is_silently_dropped() {
506 let mut allocator = Allocator::new();
507 allocator
509 .try_on_leadership_gained(1000, 1000, Epoch(5))
510 .unwrap();
511 allocator
513 .try_commit_window_extension(9_999, Epoch(4))
514 .unwrap();
515 allocator.try_grant(900, 1).unwrap();
518 assert_eq!(
519 allocator.try_grant(1_100, 1),
520 Err(CoreError::WindowExhausted)
521 );
522 }
523
524 #[test]
525 fn commit_after_leadership_lost_is_ignored() {
526 let mut allocator = Allocator::new();
527 allocator
528 .try_on_leadership_gained(1000, 5000, Epoch(1))
529 .unwrap();
530 allocator.on_leadership_lost();
531 allocator
532 .try_commit_window_extension(9_999, Epoch(1))
533 .unwrap();
534 assert!(!allocator.is_leader());
535 }
536
537 #[test]
538 fn would_grant_matches_try_grant_outcome() {
539 let mut allocator = Allocator::new();
540 assert!(!allocator.would_grant(1_000, 1));
542 allocator
544 .try_on_leadership_gained(1_000, 5_000, Epoch(1))
545 .unwrap();
546 assert!(!allocator.would_grant(1_000, 0));
547 assert!(!allocator.would_grant(1_000, LOGICAL_MAX + 2));
548 assert!(allocator.would_grant(0, 1));
551 assert!(!allocator.would_grant(5_001, 1));
553 assert!(allocator.would_grant(2_500, 1));
555 }
556
557 #[test]
558 fn would_grant_predicts_logical_wrap_advance() {
559 let mut allocator = Allocator::new();
563 allocator
564 .try_on_leadership_gained(1_000, 1_000, Epoch(1))
565 .unwrap();
566 allocator.try_grant(1_000, LOGICAL_MAX + 1).unwrap();
571 assert!(!allocator.would_grant(1_000, 1));
574 }
575
576 #[test]
577 fn would_grant_returns_false_when_advance_exceeds_physical_max() {
578 let mut allocator = Allocator::new();
581 allocator
582 .try_on_leadership_gained(PHYSICAL_MS_MAX, PHYSICAL_MS_MAX, Epoch(1))
583 .unwrap();
584 allocator
587 .try_grant(PHYSICAL_MS_MAX, LOGICAL_MAX + 1)
588 .unwrap();
589 assert!(!allocator.would_grant(PHYSICAL_MS_MAX, 1));
590 }
591
592 #[test]
593 fn default_constructs_not_leader_allocator() {
594 let allocator = Allocator::default();
595 assert!(!allocator.is_leader());
596 assert_eq!(allocator.epoch(), None);
597 }
598
599 #[test]
600 fn logical_wraps_to_next_physical_ms() {
601 let mut allocator = Allocator::new();
602 allocator.try_on_leadership_gained(0, 0, Epoch(1)).unwrap();
604 allocator.try_commit_window_extension(10, Epoch(1)).unwrap();
605 let first = allocator.try_grant(1, LOGICAL_MAX + 1).unwrap();
607 assert_eq!(first.physical_ms, 1);
608 assert_eq!(first.logical_start, 0);
609 let second = allocator.try_grant(1, 1).unwrap();
610 assert_eq!(second.physical_ms, 2);
611 assert_eq!(second.logical_start, 0);
612 }
613}