1use crate::{Epoch, LOGICAL_MAX, PHYSICAL_MS_MAX, Timestamp};
4
5#[derive(Copy, Clone, Debug, PartialEq, Eq)]
6pub struct WindowGrant {
7 pub physical_ms: u64,
8 pub logical_start: u32,
9 pub count: u32,
10 pub epoch: Epoch,
11}
12
13impl WindowGrant {
14 pub fn first(&self) -> Timestamp {
15 Timestamp::pack(self.physical_ms, self.logical_start)
16 }
17 pub fn last(&self) -> Timestamp {
18 Timestamp::pack(self.physical_ms, self.logical_start + self.count - 1)
19 }
20}
21
22#[derive(Debug, thiserror::Error, PartialEq, Eq)]
23pub enum CoreError {
24 #[error("not leader")]
25 NotLeader,
26 #[error("window exhausted; caller must extend before retrying")]
27 WindowExhausted,
28 #[error("invalid count: {0}")]
29 InvalidCount(u32),
30 #[error("physical_ms {0} exceeds 46-bit maximum")]
31 PhysicalMsOutOfRange(u64),
32 #[error(
33 "invalid leadership window: fence_floor {fence_floor} exceeds committed_ceiling {committed_ceiling}"
34 )]
35 InvalidLeadershipWindow {
36 fence_floor: u64,
37 committed_ceiling: u64,
38 },
39}
40
41#[derive(Debug)]
42enum State {
43 NotLeader,
44 Leader {
45 epoch: Epoch,
46 committed_high_water: u64,
49 next_physical_ms: u64,
53 next_logical: u32,
55 },
56}
57
58pub struct Allocator {
59 state: State,
60}
61
62impl Allocator {
63 pub fn new() -> Self {
64 Allocator {
65 state: State::NotLeader,
66 }
67 }
68
69 pub fn try_on_leadership_gained(
81 &mut self,
82 fence_floor: u64,
83 committed_ceiling: u64,
84 epoch: Epoch,
85 ) -> Result<(), CoreError> {
86 if fence_floor > PHYSICAL_MS_MAX {
87 return Err(CoreError::PhysicalMsOutOfRange(fence_floor));
88 }
89 if committed_ceiling > PHYSICAL_MS_MAX {
90 return Err(CoreError::PhysicalMsOutOfRange(committed_ceiling));
91 }
92 if committed_ceiling < fence_floor {
93 return Err(CoreError::InvalidLeadershipWindow {
94 fence_floor,
95 committed_ceiling,
96 });
97 }
98 self.state = State::Leader {
99 epoch,
100 committed_high_water: committed_ceiling,
101 next_physical_ms: fence_floor,
102 next_logical: 0,
103 };
104 Ok(())
105 }
106
107 pub fn on_leadership_gained(&mut self, fence_floor: u64, committed_ceiling: u64, epoch: Epoch) {
108 self.try_on_leadership_gained(fence_floor, committed_ceiling, epoch)
109 .expect("invalid leadership window");
110 }
111
112 pub fn on_leadership_lost(&mut self) {
113 self.state = State::NotLeader;
114 }
115
116 pub fn is_leader(&self) -> bool {
117 matches!(self.state, State::Leader { .. })
118 }
119
120 pub fn epoch(&self) -> Option<Epoch> {
121 match self.state {
122 State::Leader { epoch, .. } => Some(epoch),
123 State::NotLeader => None,
124 }
125 }
126
127 pub fn try_grant(&mut self, now_ms: u64, count: u32) -> Result<WindowGrant, CoreError> {
132 if count == 0 {
133 return Err(CoreError::InvalidCount(0));
134 }
135 if count > LOGICAL_MAX + 1 {
136 return Err(CoreError::InvalidCount(count));
137 }
138 let State::Leader {
139 epoch,
140 committed_high_water,
141 next_physical_ms,
142 next_logical,
143 } = &mut self.state
144 else {
145 return Err(CoreError::NotLeader);
146 };
147
148 if now_ms > *next_physical_ms {
151 *next_physical_ms = now_ms;
152 *next_logical = 0;
153 }
154
155 if *next_logical as u64 + count as u64 > LOGICAL_MAX as u64 + 1 {
158 *next_physical_ms += 1;
159 *next_logical = 0;
160 }
161
162 if *next_physical_ms > PHYSICAL_MS_MAX {
163 return Err(CoreError::PhysicalMsOutOfRange(*next_physical_ms));
164 }
165
166 if *next_physical_ms > *committed_high_water {
169 return Err(CoreError::WindowExhausted);
170 }
171
172 let grant = WindowGrant {
173 physical_ms: *next_physical_ms,
174 logical_start: *next_logical,
175 count,
176 epoch: *epoch,
177 };
178 *next_logical += count;
179 Ok(grant)
180 }
181
182 pub fn would_grant(&self, now_ms: u64, count: u32) -> bool {
190 if count == 0 || count > LOGICAL_MAX + 1 {
191 return false;
192 }
193 let State::Leader {
194 committed_high_water,
195 next_physical_ms,
196 next_logical,
197 ..
198 } = &self.state
199 else {
200 return false;
201 };
202
203 let mut physical_ms = *next_physical_ms;
204 let mut logical = *next_logical;
205 if now_ms > physical_ms {
206 physical_ms = now_ms;
207 logical = 0;
208 }
209 if logical as u64 + count as u64 > LOGICAL_MAX as u64 + 1 {
210 physical_ms += 1;
211 }
212 if physical_ms > PHYSICAL_MS_MAX {
213 return false;
214 }
215 physical_ms <= *committed_high_water
216 }
217
218 pub fn try_prepare_window_extension(
225 &self,
226 now_ms: u64,
227 ahead_ms: u64,
228 ) -> Result<u64, CoreError> {
229 match &self.state {
230 State::NotLeader => Ok(0),
231 State::Leader {
232 committed_high_water,
233 ..
234 } => {
235 let floor = committed_high_water
236 .checked_add(1)
237 .ok_or(CoreError::PhysicalMsOutOfRange(*committed_high_water))?;
238 let requested = core::cmp::max(floor, now_ms)
239 .checked_add(ahead_ms)
240 .ok_or(CoreError::PhysicalMsOutOfRange(u64::MAX))?;
241 if requested > PHYSICAL_MS_MAX {
242 return Err(CoreError::PhysicalMsOutOfRange(requested));
243 }
244 Ok(requested)
245 }
246 }
247 }
248
249 pub fn prepare_window_extension(&self, now_ms: u64, ahead_ms: u64) -> u64 {
250 self.try_prepare_window_extension(now_ms, ahead_ms)
251 .expect("window extension exceeds timestamp physical_ms range")
252 }
253
254 pub fn try_commit_window_extension(
265 &mut self,
266 persisted_high_water: u64,
267 expected_epoch: Epoch,
268 ) -> Result<(), CoreError> {
269 if persisted_high_water > PHYSICAL_MS_MAX {
270 return Err(CoreError::PhysicalMsOutOfRange(persisted_high_water));
271 }
272 if let State::Leader {
273 epoch,
274 committed_high_water,
275 ..
276 } = &mut self.state
277 && *epoch == expected_epoch
278 && persisted_high_water > *committed_high_water
279 {
280 *committed_high_water = persisted_high_water;
281 }
282 Ok(())
283 }
284
285 pub fn commit_window_extension(&mut self, persisted_high_water: u64, expected_epoch: Epoch) {
286 self.try_commit_window_extension(persisted_high_water, expected_epoch)
287 .expect("persisted high-water exceeds timestamp physical_ms range");
288 }
289}
290
291impl Default for Allocator {
292 fn default() -> Self {
293 Self::new()
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 #[test]
302 fn new_allocator_is_not_leader() {
303 let allocator = Allocator::new();
304 assert!(!allocator.is_leader());
305 assert_eq!(allocator.epoch(), None);
306 }
307
308 #[test]
309 fn on_leadership_gained_sets_epoch() {
310 let mut allocator = Allocator::new();
311 allocator.on_leadership_gained(1000, 5000, Epoch(5));
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!(
324 allocator.try_on_leadership_gained(5000, 4000, Epoch(5)),
325 Err(CoreError::InvalidLeadershipWindow {
326 fence_floor: 5000,
327 committed_ceiling: 4000
328 })
329 );
330 }
331
332 #[test]
333 fn on_leadership_lost_clears_state() {
334 let mut allocator = Allocator::new();
335 allocator.on_leadership_gained(1000, 5000, Epoch(5));
336 allocator.on_leadership_lost();
337 assert!(!allocator.is_leader());
338 assert_eq!(allocator.epoch(), None);
339 }
340
341 #[test]
342 fn try_grant_not_leader() {
343 let mut allocator = Allocator::new();
344 assert_eq!(allocator.try_grant(1000, 1), Err(CoreError::NotLeader));
345 }
346
347 #[test]
348 fn try_grant_zero_count() {
349 let mut allocator = Allocator::new();
350 allocator.on_leadership_gained(1000, 5000, Epoch(1));
351 assert_eq!(
352 allocator.try_grant(1000, 0),
353 Err(CoreError::InvalidCount(0))
354 );
355 }
356
357 #[test]
358 fn try_grant_above_committed_is_window_exhausted() {
359 let mut allocator = Allocator::new();
362 allocator.on_leadership_gained(5_000, 5_000, Epoch(1));
364 allocator.try_grant(4_999, 1).unwrap();
366 assert_eq!(
368 allocator.try_grant(5_001, 1),
369 Err(CoreError::WindowExhausted)
370 );
371 }
372
373 #[test]
374 fn try_grant_after_gain_serves_immediately() {
375 let mut allocator = Allocator::new();
378 allocator.on_leadership_gained(5_000, 10_000, Epoch(1));
379 let grant = allocator.try_grant(1_000, 1).unwrap();
380 assert_eq!(grant.physical_ms, 5_000);
382 assert_eq!(grant.logical_start, 0);
383 assert_eq!(grant.epoch, Epoch(1));
384 }
385
386 #[test]
387 fn prepare_window_extension_uses_now_ms_when_ahead_of_high_water() {
388 let mut allocator = Allocator::new();
389 allocator.on_leadership_gained(1000, 1000, Epoch(1));
390 let target = allocator.prepare_window_extension(2000, 3000);
391 assert_eq!(target, 5000); }
393
394 #[test]
395 fn prepare_window_extension_uses_high_water_floor_when_clock_behind() {
396 let mut allocator = Allocator::new();
397 allocator.on_leadership_gained(10_000, 10_000, Epoch(1));
398 let target = allocator.prepare_window_extension(500, 3000);
399 assert_eq!(target, 13_001);
401 }
402
403 #[test]
404 fn prepare_window_extension_rejects_out_of_range_target() {
405 let mut allocator = Allocator::new();
406 allocator.on_leadership_gained(PHYSICAL_MS_MAX, PHYSICAL_MS_MAX, Epoch(1));
407 assert_eq!(
408 allocator.try_prepare_window_extension(PHYSICAL_MS_MAX, 1),
409 Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 2))
410 );
411 }
412
413 #[test]
414 fn commit_then_try_grant_succeeds() {
415 let mut allocator = Allocator::new();
416 allocator.on_leadership_gained(1000, 1000, Epoch(7));
417 let target = allocator.prepare_window_extension(1000, 3000);
418 allocator.commit_window_extension(target, Epoch(7));
419 let grant = allocator.try_grant(1000, 5).unwrap();
420 assert_eq!(grant.count, 5);
421 assert_eq!(grant.logical_start, 0);
422 assert_eq!(grant.epoch, Epoch(7));
423 assert!(grant.physical_ms <= target);
425 }
426
427 #[test]
428 fn commit_with_lower_value_is_ignored() {
429 let mut allocator = Allocator::new();
430 allocator.on_leadership_gained(1000, 1000, Epoch(1));
431 allocator.commit_window_extension(5000, Epoch(1));
432 allocator.commit_window_extension(3000, Epoch(1)); let grant = allocator.try_grant(4500, 1).unwrap();
435 assert_eq!(grant.physical_ms, 4500);
436 }
437
438 #[test]
439 fn commit_rejects_out_of_range_high_water() {
440 let mut allocator = Allocator::new();
441 allocator.on_leadership_gained(1000, 1000, Epoch(1));
442 assert_eq!(
443 allocator.try_commit_window_extension(PHYSICAL_MS_MAX + 1, Epoch(1)),
444 Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1))
445 );
446 }
447
448 #[test]
449 fn try_grant_rejects_out_of_range_clock() {
450 let mut allocator = Allocator::new();
451 allocator.on_leadership_gained(1000, PHYSICAL_MS_MAX, Epoch(1));
452 assert_eq!(
453 allocator.try_grant(PHYSICAL_MS_MAX + 1, 1),
454 Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1))
455 );
456 }
457
458 #[test]
459 fn commit_at_wrong_epoch_is_silently_dropped() {
460 let mut allocator = Allocator::new();
461 allocator.on_leadership_gained(1000, 1000, Epoch(5));
463 allocator.commit_window_extension(9_999, Epoch(4));
465 allocator.try_grant(900, 1).unwrap();
468 assert_eq!(
469 allocator.try_grant(1_100, 1),
470 Err(CoreError::WindowExhausted)
471 );
472 }
473
474 #[test]
475 fn commit_after_leadership_lost_is_ignored() {
476 let mut allocator = Allocator::new();
477 allocator.on_leadership_gained(1000, 5000, Epoch(1));
478 allocator.on_leadership_lost();
479 allocator.commit_window_extension(9_999, Epoch(1));
480 assert!(!allocator.is_leader());
481 }
482
483 #[test]
484 fn logical_wraps_to_next_physical_ms() {
485 let mut allocator = Allocator::new();
486 allocator.on_leadership_gained(0, 0, Epoch(1));
488 allocator.commit_window_extension(10, Epoch(1));
489 let first = allocator.try_grant(1, LOGICAL_MAX + 1).unwrap();
491 assert_eq!(first.physical_ms, 1);
492 assert_eq!(first.logical_start, 0);
493 let second = allocator.try_grant(1, 1).unwrap();
494 assert_eq!(second.physical_ms, 2);
495 assert_eq!(second.logical_start, 0);
496 }
497}