tsoracle_core/allocator.rs
1//
2// ░▀█▀░█▀▀░█▀█░█▀▄░█▀█░█▀▀░█░░░█▀▀
3// ░░█░░▀▀█░█░█░█▀▄░█▀█░█░░░█░░░█▀▀
4// ░░▀░░▀▀▀░▀▀▀░▀░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀
5//
6// tsoracle — Distributed Timestamp Oracle
7// https://www.tsoracle.rs
8//
9// Copyright (c) 2026 Prisma Risk
10//
11// Licensed under the Apache License, Version 2.0 (the "License");
12// you may not use this file except in compliance with the License.
13// You may obtain a copy of the License at
14//
15// https://www.apache.org/licenses/LICENSE-2.0
16//
17// Unless required by applicable law or agreed to in writing, software
18// distributed under the License is distributed on an "AS IS" BASIS,
19// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20// See the License for the specific language governing permissions and
21// limitations under the License.
22//
23
24// #[PerformanceCriticalPath]
25//! The window allocator state machine. Sync, no I/O.
26
27use crate::{Epoch, LOGICAL_MAX, PHYSICAL_MS_MAX, Timestamp};
28
29/// A contiguous block of `count` timestamps starting at
30/// `(physical_ms, logical_start)`, all sharing one leadership `epoch`.
31///
32/// Fields are private and the only public constructor is
33/// [`try_new`](Self::try_new), which validates that every timestamp the grant
34/// covers fits the packed 46-bit physical / 18-bit logical layout. A value of
35/// this type is therefore proof that [`first`](Self::first) and
36/// [`last`](Self::last) can pack without panicking — the in-range invariant is
37/// guaranteed by the type, not by the one constructor that happens to build it.
38#[derive(Copy, Clone, Debug, PartialEq, Eq)]
39pub struct WindowGrant {
40 physical_ms: u64,
41 logical_start: u32,
42 count: u32,
43 epoch: Epoch,
44}
45
46impl WindowGrant {
47 /// Construct a grant, checking that every timestamp it covers packs
48 /// cleanly. This is the only way to build a `WindowGrant` outside this
49 /// module, so a constructed value witnesses that `first`/`last` are
50 /// infallible.
51 ///
52 /// Rejects `count == 0` (a grant covers at least one timestamp, and
53 /// `last`'s `logical_start + count - 1` would underflow). The range check
54 /// defers to [`Timestamp::try_pack`] on the *last* logical the grant emits:
55 /// it is the single source of truth for the bit layout, and since
56 /// `logical_start <= last_logical <= LOGICAL_MAX` validating the last
57 /// boundary validates the first by implication.
58 pub fn try_new(
59 physical_ms: u64,
60 logical_start: u32,
61 count: u32,
62 epoch: Epoch,
63 ) -> Result<Self, CoreError> {
64 if count == 0 {
65 return Err(CoreError::InvalidCount(0));
66 }
67 let last_logical =
68 logical_start
69 .checked_add(count - 1)
70 .ok_or(CoreError::LogicalRangeOutOfRange {
71 logical_start,
72 count,
73 })?;
74 Timestamp::try_pack(physical_ms, last_logical).map_err(|err| match err {
75 crate::TimestampError::PhysicalMsOutOfRange { physical_ms, .. } => {
76 CoreError::PhysicalMsOutOfRange(physical_ms)
77 }
78 crate::TimestampError::LogicalOutOfRange { .. } => CoreError::LogicalRangeOutOfRange {
79 logical_start,
80 count,
81 },
82 })?;
83 Ok(WindowGrant {
84 physical_ms,
85 logical_start,
86 count,
87 epoch,
88 })
89 }
90
91 pub fn physical_ms(&self) -> u64 {
92 self.physical_ms
93 }
94 pub fn logical_start(&self) -> u32 {
95 self.logical_start
96 }
97 pub fn count(&self) -> u32 {
98 self.count
99 }
100 pub fn epoch(&self) -> Epoch {
101 self.epoch
102 }
103
104 /// The first timestamp in the grant. Infallible: [`try_new`](Self::try_new)
105 /// validated `(physical_ms, logical_start)` is in range, so `pack` cannot
106 /// trip its `assert!`.
107 pub fn first(&self) -> Timestamp {
108 Timestamp::pack(self.physical_ms, self.logical_start)
109 }
110 /// The last timestamp in the grant. Infallible: [`try_new`](Self::try_new)
111 /// validated `(physical_ms, logical_start + count - 1)` is in range (and
112 /// `count >= 1`, so the subtraction cannot underflow), so `pack` cannot
113 /// trip its `assert!`.
114 pub fn last(&self) -> Timestamp {
115 Timestamp::pack(self.physical_ms, self.logical_start + self.count - 1)
116 }
117}
118
119#[derive(Debug, thiserror::Error, PartialEq, Eq)]
120pub enum CoreError {
121 #[error("not leader")]
122 NotLeader,
123 #[error("window exhausted; caller must extend before retrying")]
124 WindowExhausted,
125 #[error("invalid count: {0}")]
126 InvalidCount(u32),
127 #[error("physical_ms {0} exceeds 46-bit maximum")]
128 PhysicalMsOutOfRange(u64),
129 #[error("logical range [{logical_start}, +{count}) exceeds the 18-bit logical field")]
130 LogicalRangeOutOfRange { logical_start: u32, count: u32 },
131 #[error(
132 "invalid leadership window: fence_floor {fence_floor} exceeds committed_ceiling {committed_ceiling}"
133 )]
134 InvalidLeadershipWindow {
135 fence_floor: u64,
136 committed_ceiling: u64,
137 },
138 #[error(
139 "window extension overflow: max(floor {floor}, now_ms {now_ms}) + ahead_ms {ahead_ms} exceeds u64::MAX"
140 )]
141 WindowExtensionOverflow {
142 floor: u64,
143 now_ms: u64,
144 ahead_ms: u64,
145 },
146}
147
148/// The result of a `try_commit_window_extension` that passed range validation.
149///
150/// A commit either raises the durable bound or is dropped for one of three
151/// benign, expected reasons. Collapsing both into `Ok(())` left a caller that
152/// just paid for a `persist_high_water` round-trip unable to tell "I raised the
153/// bound" from "I silently dropped your durably-persisted value." This type
154/// preserves the distinction so the server can log/metric the dropped commits —
155/// a leading indicator of epoch churn or persist reordering.
156#[derive(Copy, Clone, Debug, PartialEq, Eq)]
157pub enum CommitOutcome {
158 /// The durable bound advanced to this value.
159 Applied(u64),
160 /// The bound did not move; see [`IgnoreReason`] for why.
161 Ignored(IgnoreReason),
162}
163
164/// Why a [`CommitOutcome::Ignored`] commit left the durable bound unchanged.
165///
166/// All three are benign and expected under normal failover: the epoch-fencing
167/// design of `try_commit_window_extension` deliberately drops late commits from
168/// a superseded epoch rather than erroring, and the monotonic bound rejects a
169/// value that does not advance. They are kept apart so a caller can distinguish
170/// epoch churn ([`NotLeader`](Self::NotLeader) / [`EpochMismatch`](Self::EpochMismatch))
171/// from persist reordering ([`NotAdvanced`](Self::NotAdvanced)).
172#[derive(Copy, Clone, Debug, PartialEq, Eq)]
173pub enum IgnoreReason {
174 /// The allocator is no longer a leader, so the commit has no window to raise.
175 NotLeader,
176 /// The allocator leads a different epoch than the commit targeted; the
177 /// commit is a late persist from a superseded epoch, fenced out.
178 EpochMismatch { expected: Epoch, current: Epoch },
179 /// The allocator still leads the targeted epoch, but the persisted value did
180 /// not exceed the current bound, so the monotonic bound rejects it.
181 NotAdvanced { persisted: u64, committed: u64 },
182}
183
184#[derive(Debug)]
185enum State {
186 NotLeader,
187 Leader {
188 epoch: Epoch,
189 /// Persisted upper bound: the allocator will not issue any timestamp with
190 /// `physical_ms` greater than this without a fresh `try_commit_window_extension`.
191 committed_high_water: u64,
192 /// Next `physical_ms` we are willing to issue at. Initialized to
193 /// `fence_floor` on leadership gain, then advances monotonically — never
194 /// retreats below the fence even when `now_ms` is a past value.
195 next_physical_ms: u64,
196 /// Next logical counter within `next_physical_ms`.
197 next_logical: u32,
198 },
199}
200
201pub struct Allocator {
202 state: State,
203}
204
205impl Allocator {
206 pub fn new() -> Self {
207 Allocator {
208 state: State::NotLeader,
209 }
210 }
211
212 /// Seed the allocator once the failover fence has durably persisted both
213 /// the floor and the pre-extended ceiling.
214 ///
215 /// `fence_floor` is the first `physical_ms` the new leader may issue —
216 /// the server sets it to `prior_high_water + 1` so the new leader's
217 /// timestamps are strictly above any the prior leader could have issued.
218 ///
219 /// `committed_ceiling` is the pre-extended upper bound the server has
220 /// already persisted (typically `fence_floor + window_ms`). It must
221 /// satisfy `committed_ceiling >= fence_floor` so the allocator can serve
222 /// `try_grant` immediately without an additional extension round-trip.
223 pub fn try_on_leadership_gained(
224 &mut self,
225 fence_floor: u64,
226 committed_ceiling: u64,
227 epoch: Epoch,
228 ) -> Result<(), CoreError> {
229 if fence_floor > PHYSICAL_MS_MAX {
230 return Err(CoreError::PhysicalMsOutOfRange(fence_floor));
231 }
232 if committed_ceiling > PHYSICAL_MS_MAX {
233 return Err(CoreError::PhysicalMsOutOfRange(committed_ceiling));
234 }
235 if committed_ceiling < fence_floor {
236 return Err(CoreError::InvalidLeadershipWindow {
237 fence_floor,
238 committed_ceiling,
239 });
240 }
241 self.state = State::Leader {
242 epoch,
243 committed_high_water: committed_ceiling,
244 next_physical_ms: fence_floor,
245 next_logical: 0,
246 };
247 Ok(())
248 }
249
250 pub fn on_leadership_lost(&mut self) {
251 self.state = State::NotLeader;
252 }
253
254 pub fn is_leader(&self) -> bool {
255 matches!(self.state, State::Leader { .. })
256 }
257
258 pub fn epoch(&self) -> Option<Epoch> {
259 match self.state {
260 State::Leader { epoch, .. } => Some(epoch),
261 State::NotLeader => None,
262 }
263 }
264
265 /// Current committed high-water in physical-millisecond units, or `None`
266 /// when not the leader. The high-water is the upper bound the allocator
267 /// will not exceed without a fresh `try_commit_window_extension`.
268 pub fn committed_high_water(&self) -> Option<u64> {
269 match self.state {
270 State::Leader {
271 committed_high_water,
272 ..
273 } => Some(committed_high_water),
274 State::NotLeader => None,
275 }
276 }
277
278 /// Shared count guard for `try_grant` and `would_grant`, kept here so the
279 /// two entry points cannot drift. The server's extension single-flight
280 /// relies on `would_grant(now_ms, count) == true` implying the retry
281 /// `try_grant(now_ms, count)` succeeds (see `service::extend_window`), so
282 /// the set of rejected counts must be identical on both paths — splitting
283 /// this check across both methods would let a future edit to one degrade
284 /// the recheck into a spurious consensus round-trip (or worse).
285 ///
286 /// `count == 0` reuses the oversized case's `InvalidCount(count)`; since
287 /// `count` is `0` there, the surfaced value matches the prior explicit
288 /// `InvalidCount(0)`.
289 fn validate_count(count: u32) -> Result<(), CoreError> {
290 if count == 0 || count > LOGICAL_MAX + 1 {
291 return Err(CoreError::InvalidCount(count));
292 }
293 Ok(())
294 }
295
296 /// Single source of truth for the window-advance simulation and its bounds
297 /// checks, shared by `try_grant` and `would_grant`. Pure: it takes the
298 /// relevant state fields by value and mutates nothing, so a failed
299 /// projection cannot leave allocator state advanced.
300 ///
301 /// On success returns the `(physical_ms, logical_start)` the grant would
302 /// occupy. The two failure variants are kept distinct so `try_grant` can
303 /// surface the precise error its callers (and tests) expect;
304 /// `would_grant` collapses both to `false` via `.is_ok()`.
305 fn project_grant(
306 next_physical_ms: u64,
307 next_logical: u32,
308 committed_high_water: u64,
309 now_ms: u64,
310 count: u32,
311 ) -> Result<(u64, u32), CoreError> {
312 let mut physical_ms = next_physical_ms;
313 let mut logical = next_logical;
314
315 // Advance physical_ms toward wall clock if ahead. next_physical_ms is
316 // already at or above fence_floor, so a low now_ms simply leaves it there.
317 if now_ms > physical_ms {
318 physical_ms = now_ms;
319 logical = 0;
320 }
321
322 // If the current physical_ms cannot fit the request in its remaining
323 // logical range, advance to the next physical_ms.
324 if logical as u64 + count as u64 > LOGICAL_MAX as u64 + 1 {
325 physical_ms += 1;
326 logical = 0;
327 }
328
329 if physical_ms > PHYSICAL_MS_MAX {
330 return Err(CoreError::PhysicalMsOutOfRange(physical_ms));
331 }
332
333 // The fence: never issue a timestamp at a physical_ms above the committed
334 // high-water. If we are at or past the bound, the caller must extend.
335 if physical_ms > committed_high_water {
336 return Err(CoreError::WindowExhausted);
337 }
338
339 Ok((physical_ms, logical))
340 }
341
342 /// Hot path. Issue `count` timestamps from the in-memory window.
343 ///
344 /// Returns `WindowExhausted` when the in-memory remainder cannot cover the request;
345 /// the caller (typically the server) then runs prepare → persist → commit and retries.
346 ///
347 /// State is written back only on success: a failed grant (out-of-range or
348 /// exhausted window) leaves `next_physical_ms`/`next_logical` untouched.
349 pub fn try_grant(&mut self, now_ms: u64, count: u32) -> Result<WindowGrant, CoreError> {
350 Self::validate_count(count)?;
351 let State::Leader {
352 epoch,
353 committed_high_water,
354 next_physical_ms,
355 next_logical,
356 } = &mut self.state
357 else {
358 return Err(CoreError::NotLeader);
359 };
360
361 let (physical_ms, logical_start) = Self::project_grant(
362 *next_physical_ms,
363 *next_logical,
364 *committed_high_water,
365 now_ms,
366 count,
367 )?;
368
369 // project_grant already guarantees physical_ms and the logical range
370 // are in bounds, so this construction never fails in practice — but
371 // routing through the checked constructor propagates a CoreError rather
372 // than panicking should that invariant ever be violated, and keeps this
373 // path free of the unwrap/expect the crate's panic policy bans.
374 let grant = WindowGrant::try_new(physical_ms, logical_start, count, *epoch)?;
375 // Normalize the write-back so the stored cursor is always a packable
376 // (physical_ms, logical) pair. `project_grant` admits a grant that fills
377 // the millisecond exactly, so `logical_start + count` can reach
378 // LOGICAL_MAX + 1 — a logical the packed layout cannot hold. Rather than
379 // store that sentinel and rely on the next `project_grant` call to wrap
380 // it, roll to the next millisecond at logical 0 here. This is precisely
381 // the position the next call would compute, so behavior is unchanged;
382 // the stored state just no longer depends on the implicit
383 // "next call always wraps or resets" invariant. (`next_physical_ms` may
384 // become PHYSICAL_MS_MAX + 1, which is never packed directly and is
385 // rejected as out-of-range by the next grant's `project_grant`.)
386 let next_logical_after = logical_start + count;
387 if next_logical_after > LOGICAL_MAX {
388 *next_physical_ms = physical_ms + 1;
389 *next_logical = 0;
390 } else {
391 *next_physical_ms = physical_ms;
392 *next_logical = next_logical_after;
393 }
394 Ok(grant)
395 }
396
397 /// Non-mutating predicate: would `try_grant(now_ms, count)` succeed right
398 /// now? Used by the server's extension single-flight to decide whether a
399 /// peer extender has already added enough room, avoiding a redundant
400 /// `persist_high_water` round-trip. Delegates to the same `project_grant`
401 /// helper `try_grant` uses, so the exhaustion check cannot drift — a
402 /// coarser predicate would risk false positives (skip the extension, then
403 /// fail the outer retry) for requests whose `count` straddles the window edge.
404 pub fn would_grant(&self, now_ms: u64, count: u32) -> bool {
405 if Self::validate_count(count).is_err() {
406 return false;
407 }
408 let State::Leader {
409 committed_high_water,
410 next_physical_ms,
411 next_logical,
412 ..
413 } = &self.state
414 else {
415 return false;
416 };
417
418 Self::project_grant(
419 *next_physical_ms,
420 *next_logical,
421 *committed_high_water,
422 now_ms,
423 count,
424 )
425 .is_ok()
426 }
427
428 /// Compute the high-water value the caller should durably persist before
429 /// calling `try_commit_window_extension`. Does not mutate.
430 ///
431 /// Returns `max(committed_high_water + 1, now_ms) + ahead_ms`. The +1 on
432 /// `committed_high_water` guarantees forward progress when wall clock is
433 /// behind the persisted bound (rare, but possible after a clock-step-back).
434 ///
435 /// Returns `Err(CoreError::NotLeader)` off-leader, matching every other
436 /// mutating method. A `0` sentinel here would be indistinguishable from a
437 /// legitimately prepared bound, letting a caller that skipped `is_leader()`
438 /// proceed as if preparation had succeeded.
439 pub fn try_prepare_window_extension(
440 &self,
441 now_ms: u64,
442 ahead_ms: u64,
443 ) -> Result<u64, CoreError> {
444 match &self.state {
445 State::NotLeader => Err(CoreError::NotLeader),
446 State::Leader {
447 committed_high_water,
448 ..
449 } => {
450 let floor = committed_high_water
451 .checked_add(1)
452 .ok_or(CoreError::PhysicalMsOutOfRange(*committed_high_water))?;
453 let requested = core::cmp::max(floor, now_ms).checked_add(ahead_ms).ok_or(
454 CoreError::WindowExtensionOverflow {
455 floor,
456 now_ms,
457 ahead_ms,
458 },
459 )?;
460 if requested > PHYSICAL_MS_MAX {
461 return Err(CoreError::PhysicalMsOutOfRange(requested));
462 }
463 Ok(requested)
464 }
465 }
466 }
467
468 /// Apply a durably-persisted window extension. `persisted_high_water` is
469 /// the value returned by `ConsensusDriver::persist_high_water`, which is
470 /// monotonic — it may equal or exceed the value passed to prepare.
471 ///
472 /// The `expected_epoch` argument fences out late-arriving commits from a
473 /// prior leader epoch: if the allocator is no longer at this epoch (either
474 /// it has lost leadership or a new leader took over), the commit is
475 /// dropped. Combined with the server's drain barrier, this guarantees a
476 /// late persist from epoch N cannot raise the durable bound observed by
477 /// epoch N+M.
478 ///
479 /// Returns [`CommitOutcome`]: `Applied` when the bound advanced, or
480 /// `Ignored` (with the reason) for the three benign drop cases. A value
481 /// exceeding the 46-bit physical ceiling is an invariant violation, not a
482 /// benign drop, so it stays `Err(PhysicalMsOutOfRange)`.
483 pub fn try_commit_window_extension(
484 &mut self,
485 persisted_high_water: u64,
486 expected_epoch: Epoch,
487 ) -> Result<CommitOutcome, CoreError> {
488 if persisted_high_water > PHYSICAL_MS_MAX {
489 return Err(CoreError::PhysicalMsOutOfRange(persisted_high_water));
490 }
491 let State::Leader {
492 epoch,
493 committed_high_water,
494 ..
495 } = &mut self.state
496 else {
497 return Ok(CommitOutcome::Ignored(IgnoreReason::NotLeader));
498 };
499 // Epoch fencing takes precedence over the monotonic check: a late
500 // persist from a superseded epoch must report EpochMismatch even when
501 // its value also fails to advance, so churn is not masked as reordering.
502 if *epoch != expected_epoch {
503 return Ok(CommitOutcome::Ignored(IgnoreReason::EpochMismatch {
504 expected: expected_epoch,
505 current: *epoch,
506 }));
507 }
508 if persisted_high_water <= *committed_high_water {
509 return Ok(CommitOutcome::Ignored(IgnoreReason::NotAdvanced {
510 persisted: persisted_high_water,
511 committed: *committed_high_water,
512 }));
513 }
514 *committed_high_water = persisted_high_water;
515 Ok(CommitOutcome::Applied(persisted_high_water))
516 }
517}
518
519impl Default for Allocator {
520 fn default() -> Self {
521 Self::new()
522 }
523}
524
525#[cfg(test)]
526mod tests {
527 use super::*;
528
529 #[test]
530 fn new_allocator_is_not_leader() {
531 let allocator = Allocator::new();
532 assert!(!allocator.is_leader());
533 assert_eq!(allocator.epoch(), None);
534 }
535
536 #[test]
537 fn on_leadership_gained_sets_epoch() {
538 let mut allocator = Allocator::new();
539 allocator
540 .try_on_leadership_gained(1000, 5000, Epoch(5))
541 .unwrap();
542 assert!(allocator.is_leader());
543 assert_eq!(allocator.epoch(), Some(Epoch(5)));
544 }
545
546 #[test]
547 fn try_on_leadership_gained_rejects_out_of_range_window() {
548 let mut allocator = Allocator::new();
549 assert_eq!(
550 allocator.try_on_leadership_gained(PHYSICAL_MS_MAX + 1, PHYSICAL_MS_MAX + 1, Epoch(5)),
551 Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1))
552 );
553 // fence_floor in-range, ceiling out-of-range — separate guard.
554 assert_eq!(
555 allocator.try_on_leadership_gained(1_000, PHYSICAL_MS_MAX + 1, Epoch(5)),
556 Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1))
557 );
558 assert_eq!(
559 allocator.try_on_leadership_gained(5000, 4000, Epoch(5)),
560 Err(CoreError::InvalidLeadershipWindow {
561 fence_floor: 5000,
562 committed_ceiling: 4000
563 })
564 );
565 }
566
567 #[test]
568 fn on_leadership_lost_clears_state() {
569 let mut allocator = Allocator::new();
570 allocator
571 .try_on_leadership_gained(1000, 5000, Epoch(5))
572 .unwrap();
573 allocator.on_leadership_lost();
574 assert!(!allocator.is_leader());
575 assert_eq!(allocator.epoch(), None);
576 }
577
578 #[test]
579 fn committed_high_water_tracks_leader_state_and_extensions() {
580 let mut allocator = Allocator::new();
581 assert_eq!(allocator.committed_high_water(), None);
582
583 allocator
584 .try_on_leadership_gained(1_000, 5_000, Epoch(1))
585 .unwrap();
586 assert_eq!(allocator.committed_high_water(), Some(5_000));
587
588 let target = allocator
589 .try_prepare_window_extension(2_000, 3_000)
590 .unwrap();
591 allocator
592 .try_commit_window_extension(target, Epoch(1))
593 .unwrap();
594 assert_eq!(allocator.committed_high_water(), Some(target));
595
596 allocator.on_leadership_lost();
597 assert_eq!(allocator.committed_high_water(), None);
598 }
599
600 #[test]
601 fn try_grant_not_leader() {
602 let mut allocator = Allocator::new();
603 assert_eq!(allocator.try_grant(1000, 1), Err(CoreError::NotLeader));
604 }
605
606 #[test]
607 fn try_grant_zero_count() {
608 let mut allocator = Allocator::new();
609 allocator
610 .try_on_leadership_gained(1000, 5000, Epoch(1))
611 .unwrap();
612 assert_eq!(
613 allocator.try_grant(1000, 0),
614 Err(CoreError::InvalidCount(0))
615 );
616 }
617
618 #[test]
619 fn try_grant_oversized_count() {
620 let mut allocator = Allocator::new();
621 allocator
622 .try_on_leadership_gained(1000, 5000, Epoch(1))
623 .unwrap();
624 let oversized = LOGICAL_MAX + 2;
625 assert_eq!(
626 allocator.try_grant(1000, oversized),
627 Err(CoreError::InvalidCount(oversized))
628 );
629 }
630
631 #[test]
632 fn try_grant_above_committed_is_window_exhausted() {
633 // Advancing `now_ms` past `committed_high_water` correctly returns
634 // WindowExhausted; the server then extends.
635 let mut allocator = Allocator::new();
636 // fence_floor=5_000, ceiling=5_000 (tight window, no pre-extended gap).
637 allocator
638 .try_on_leadership_gained(5_000, 5_000, Epoch(1))
639 .unwrap();
640 // now_ms below floor: clamps to floor=5_000, which equals the ceiling → succeeds.
641 allocator.try_grant(4_999, 1).unwrap();
642 // now_ms above ceiling: window exhausted.
643 assert_eq!(
644 allocator.try_grant(5_001, 1),
645 Err(CoreError::WindowExhausted)
646 );
647 }
648
649 #[test]
650 fn failed_try_grant_does_not_advance_state() {
651 // A grant that fails the exhaustion check must leave the allocator's
652 // advance state untouched, so a later grant at a lower `now_ms` is not
653 // pinned to the failed attempt's wall clock.
654 let mut allocator = Allocator::new();
655 // Tight initial window: fence_floor == ceiling == 1_000.
656 allocator
657 .try_on_leadership_gained(1_000, 1_000, Epoch(1))
658 .unwrap();
659 // now_ms far past the ceiling exhausts the window.
660 assert_eq!(
661 allocator.try_grant(5_000, 1),
662 Err(CoreError::WindowExhausted)
663 );
664 // Extend the durable bound to exactly 2_000.
665 let target = allocator.try_prepare_window_extension(2_000, 0).unwrap();
666 assert_eq!(target, 2_000); // max(committed+1=1_001, now=2_000) + 0
667 allocator
668 .try_commit_window_extension(target, Epoch(1))
669 .unwrap();
670 // The failed grant must not have pinned next_physical_ms at 5_000: a
671 // grant at now_ms=2_000 advances cleanly to physical_ms=2_000 (<= the
672 // committed 2_000). If state had advanced on the failure, next_physical_ms
673 // would still be 5_000 and this would exhaust the window again.
674 let grant = allocator.try_grant(2_000, 1).unwrap();
675 assert_eq!(grant.physical_ms, 2_000);
676 assert_eq!(grant.logical_start, 0);
677 }
678
679 #[test]
680 fn try_grant_after_gain_serves_immediately() {
681 // The fence has already persisted a pre-extended window, so the allocator
682 // can serve immediately. Grants start at fence_floor regardless of now_ms.
683 let mut allocator = Allocator::new();
684 allocator
685 .try_on_leadership_gained(5_000, 10_000, Epoch(1))
686 .unwrap();
687 let grant = allocator.try_grant(1_000, 1).unwrap();
688 // now_ms=1_000 < fence_floor=5_000, so next_physical_ms stays at 5_000.
689 assert_eq!(grant.physical_ms, 5_000);
690 assert_eq!(grant.logical_start, 0);
691 assert_eq!(grant.epoch, Epoch(1));
692 }
693
694 #[test]
695 fn prepare_window_extension_not_leader() {
696 // Off-leader prepare must error like every other mutating method, not
697 // return a `0` that a caller could mistake for a prepared bound.
698 let allocator = Allocator::new();
699 assert_eq!(
700 allocator.try_prepare_window_extension(1000, 3000),
701 Err(CoreError::NotLeader)
702 );
703 }
704
705 #[test]
706 fn prepare_window_extension_uses_now_ms_when_ahead_of_high_water() {
707 let mut allocator = Allocator::new();
708 allocator
709 .try_on_leadership_gained(1000, 1000, Epoch(1))
710 .unwrap();
711 let target = allocator.try_prepare_window_extension(2000, 3000).unwrap();
712 assert_eq!(target, 5000); // max(1001, 2000) + 3000
713 }
714
715 #[test]
716 fn prepare_window_extension_uses_high_water_floor_when_clock_behind() {
717 let mut allocator = Allocator::new();
718 allocator
719 .try_on_leadership_gained(10_000, 10_000, Epoch(1))
720 .unwrap();
721 let target = allocator.try_prepare_window_extension(500, 3000).unwrap();
722 // floor = 10_001, clock = 500. max = 10_001. + 3000 = 13_001.
723 assert_eq!(target, 13_001);
724 }
725
726 #[test]
727 fn prepare_window_extension_rejects_out_of_range_target() {
728 let mut allocator = Allocator::new();
729 allocator
730 .try_on_leadership_gained(PHYSICAL_MS_MAX, PHYSICAL_MS_MAX, Epoch(1))
731 .unwrap();
732 assert_eq!(
733 allocator.try_prepare_window_extension(PHYSICAL_MS_MAX, 1),
734 Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 2))
735 );
736 }
737
738 #[test]
739 fn prepare_window_extension_overflow_names_all_operands() {
740 // A saturated clock (SystemClock::now_ms saturates to u64::MAX) plus any
741 // non-zero ahead_ms overflows max(floor, now_ms) + ahead_ms. The error
742 // must name the three real operands so the log points at the clock, not
743 // a phantom "someone passed an absurd physical_ms".
744 let mut allocator = Allocator::new();
745 allocator
746 .try_on_leadership_gained(1_000, 1_000, Epoch(1))
747 .unwrap();
748 assert_eq!(
749 allocator.try_prepare_window_extension(u64::MAX, 1),
750 Err(CoreError::WindowExtensionOverflow {
751 floor: 1_001,
752 now_ms: u64::MAX,
753 ahead_ms: 1,
754 })
755 );
756 }
757
758 #[test]
759 fn commit_then_try_grant_succeeds() {
760 let mut allocator = Allocator::new();
761 allocator
762 .try_on_leadership_gained(1000, 1000, Epoch(7))
763 .unwrap();
764 let target = allocator.try_prepare_window_extension(1000, 3000).unwrap();
765 assert_eq!(
766 allocator.try_commit_window_extension(target, Epoch(7)),
767 Ok(CommitOutcome::Applied(target))
768 );
769 let grant = allocator.try_grant(1000, 5).unwrap();
770 assert_eq!(grant.count, 5);
771 assert_eq!(grant.logical_start, 0);
772 assert_eq!(grant.epoch, Epoch(7));
773 // physical_ms should be at most the persisted high-water.
774 assert!(grant.physical_ms <= target);
775 }
776
777 #[test]
778 fn commit_with_lower_value_is_ignored() {
779 let mut allocator = Allocator::new();
780 allocator
781 .try_on_leadership_gained(1000, 1000, Epoch(1))
782 .unwrap();
783 assert_eq!(
784 allocator.try_commit_window_extension(5000, Epoch(1)),
785 Ok(CommitOutcome::Applied(5000))
786 );
787 // A non-advancing commit reports the values so the caller can tell a
788 // monotonic-bound regression (persist reordering) from epoch churn.
789 assert_eq!(
790 allocator.try_commit_window_extension(3000, Epoch(1)),
791 Ok(CommitOutcome::Ignored(IgnoreReason::NotAdvanced {
792 persisted: 3000,
793 committed: 5000,
794 }))
795 );
796 // try_grant up to physical_ms=5000 should still work.
797 let grant = allocator.try_grant(4500, 1).unwrap();
798 assert_eq!(grant.physical_ms, 4500);
799 }
800
801 #[test]
802 fn commit_with_equal_value_is_ignored_not_applied() {
803 // persist_high_water is monotonic and may *equal* the prepared bound; an
804 // equal value moves nothing, so it is Ignored(NotAdvanced), not Applied.
805 let mut allocator = Allocator::new();
806 allocator
807 .try_on_leadership_gained(1000, 5000, Epoch(1))
808 .unwrap();
809 assert_eq!(
810 allocator.try_commit_window_extension(5000, Epoch(1)),
811 Ok(CommitOutcome::Ignored(IgnoreReason::NotAdvanced {
812 persisted: 5000,
813 committed: 5000,
814 }))
815 );
816 }
817
818 #[test]
819 fn commit_rejects_out_of_range_high_water() {
820 let mut allocator = Allocator::new();
821 allocator
822 .try_on_leadership_gained(1000, 1000, Epoch(1))
823 .unwrap();
824 assert_eq!(
825 allocator.try_commit_window_extension(PHYSICAL_MS_MAX + 1, Epoch(1)),
826 Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1))
827 );
828 }
829
830 #[test]
831 fn try_grant_rejects_out_of_range_clock() {
832 let mut allocator = Allocator::new();
833 allocator
834 .try_on_leadership_gained(1000, PHYSICAL_MS_MAX, Epoch(1))
835 .unwrap();
836 assert_eq!(
837 allocator.try_grant(PHYSICAL_MS_MAX + 1, 1),
838 Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1))
839 );
840 }
841
842 #[test]
843 fn commit_at_wrong_epoch_is_silently_dropped() {
844 let mut allocator = Allocator::new();
845 // fence_floor=1000, ceiling=1000: tight initial window.
846 allocator
847 .try_on_leadership_gained(1000, 1000, Epoch(5))
848 .unwrap();
849 // A late persist from epoch 4 (the prior leader) — fenced out. The
850 // outcome names both epochs so the caller can metric epoch churn.
851 assert_eq!(
852 allocator.try_commit_window_extension(9_999, Epoch(4)),
853 Ok(CommitOutcome::Ignored(IgnoreReason::EpochMismatch {
854 expected: Epoch(4),
855 current: Epoch(5),
856 }))
857 );
858 // The allocator's bound did not move; a grant at now=900 clamps to
859 // floor=1000, and a request with now=1_100 exhausts the window.
860 allocator.try_grant(900, 1).unwrap();
861 assert_eq!(
862 allocator.try_grant(1_100, 1),
863 Err(CoreError::WindowExhausted)
864 );
865 }
866
867 #[test]
868 fn commit_after_leadership_lost_is_ignored() {
869 let mut allocator = Allocator::new();
870 allocator
871 .try_on_leadership_gained(1000, 5000, Epoch(1))
872 .unwrap();
873 allocator.on_leadership_lost();
874 assert_eq!(
875 allocator.try_commit_window_extension(9_999, Epoch(1)),
876 Ok(CommitOutcome::Ignored(IgnoreReason::NotLeader))
877 );
878 assert!(!allocator.is_leader());
879 }
880
881 #[test]
882 fn would_grant_matches_try_grant_outcome() {
883 let mut allocator = Allocator::new();
884 // Not leader: never grants.
885 assert!(!allocator.would_grant(1_000, 1));
886 // Invalid counts: never grants.
887 allocator
888 .try_on_leadership_gained(1_000, 5_000, Epoch(1))
889 .unwrap();
890 assert!(!allocator.would_grant(1_000, 0));
891 assert!(!allocator.would_grant(1_000, LOGICAL_MAX + 2));
892 // Within-window: matches try_grant. now_ms below floor still grants
893 // (clamped to floor=1_000, ceiling=5_000).
894 assert!(allocator.would_grant(0, 1));
895 // now_ms above ceiling: predicate refuses (would exhaust).
896 assert!(!allocator.would_grant(5_001, 1));
897 // Mid-window now_ms advances the predicate's internal physical_ms.
898 assert!(allocator.would_grant(2_500, 1));
899 }
900
901 #[test]
902 fn would_grant_predicts_logical_wrap_advance() {
903 // When (logical + count) overflows the per-ms logical range, the
904 // predicate (like try_grant) advances physical_ms by 1. If that
905 // advance leaves the window, would_grant must return false.
906 let mut allocator = Allocator::new();
907 allocator
908 .try_on_leadership_gained(1_000, 1_000, Epoch(1))
909 .unwrap();
910 // count >= LOGICAL_MAX + 1 forces the advance branch on a fresh
911 // window: logical(0) + count(LOGICAL_MAX+1) doesn't overflow on its
912 // own, but anything one bigger does. Use LOGICAL_MAX + 1 to land at
913 // the edge, then any non-zero issue advances physical_ms.
914 allocator.try_grant(1_000, LOGICAL_MAX + 1).unwrap();
915 // Next grant of size 1 would advance to physical_ms = 1_001, which
916 // exceeds the committed ceiling of 1_000.
917 assert!(!allocator.would_grant(1_000, 1));
918 }
919
920 #[test]
921 fn would_grant_returns_false_when_advance_exceeds_physical_max() {
922 // Construct an allocator at PHYSICAL_MS_MAX so the +1 advance
923 // crosses the 46-bit ceiling and the predicate refuses.
924 let mut allocator = Allocator::new();
925 allocator
926 .try_on_leadership_gained(PHYSICAL_MS_MAX, PHYSICAL_MS_MAX, Epoch(1))
927 .unwrap();
928 // Fill the logical range so the next would_grant call has to
929 // advance physical_ms.
930 allocator
931 .try_grant(PHYSICAL_MS_MAX, LOGICAL_MAX + 1)
932 .unwrap();
933 assert!(!allocator.would_grant(PHYSICAL_MS_MAX, 1));
934 }
935
936 #[test]
937 fn default_constructs_not_leader_allocator() {
938 let allocator = Allocator::default();
939 assert!(!allocator.is_leader());
940 assert_eq!(allocator.epoch(), None);
941 }
942
943 #[test]
944 fn logical_wraps_to_next_physical_ms() {
945 let mut allocator = Allocator::new();
946 // fence_floor=0, ceiling=0; extend to 10 before granting.
947 allocator.try_on_leadership_gained(0, 0, Epoch(1)).unwrap();
948 allocator.try_commit_window_extension(10, Epoch(1)).unwrap();
949 // Issue LOGICAL_MAX+1 logicals at physical_ms=1, then one more should bump to 2.
950 let first = allocator.try_grant(1, LOGICAL_MAX + 1).unwrap();
951 assert_eq!(first.physical_ms, 1);
952 assert_eq!(first.logical_start, 0);
953 let second = allocator.try_grant(1, 1).unwrap();
954 assert_eq!(second.physical_ms, 2);
955 assert_eq!(second.logical_start, 0);
956 }
957
958 #[test]
959 fn exact_fill_grant_normalizes_stored_state_to_packable() {
960 // A grant that consumes a millisecond's entire logical range
961 // (logical_start + count == LOGICAL_MAX + 1) must not leave the
962 // LOGICAL_MAX+1 sentinel in next_logical: that value cannot be packed
963 // (Timestamp::pack asserts logical <= LOGICAL_MAX), so the stored state
964 // would only be safe by the implicit "next call always wraps" invariant.
965 // The write-back normalizes it to the already-rolled position
966 // (physical_ms + 1, 0), so stored state is always directly packable.
967 let mut allocator = Allocator::new();
968 allocator.try_on_leadership_gained(0, 0, Epoch(1)).unwrap();
969 allocator.try_commit_window_extension(10, Epoch(1)).unwrap();
970 // Fill physical_ms=1 exactly: logical [0, LOGICAL_MAX].
971 let grant = allocator.try_grant(1, LOGICAL_MAX + 1).unwrap();
972 assert_eq!(grant.physical_ms, 1);
973 assert_eq!(grant.logical_start, 0);
974
975 let State::Leader {
976 next_physical_ms,
977 next_logical,
978 ..
979 } = allocator.state
980 else {
981 panic!("expected leader state after a successful grant");
982 };
983 // The stored cursor rolled to the next millisecond at logical 0 …
984 assert_eq!(next_physical_ms, 2);
985 assert_eq!(next_logical, 0);
986 // … and is, by construction, a packable timestamp.
987 assert!(Timestamp::try_pack(next_physical_ms, next_logical).is_ok());
988 }
989
990 #[test]
991 fn try_new_accepts_valid_grant_and_packs_boundaries() {
992 // A checked grant exposes its fields through accessors and its
993 // boundary timestamps pack without panicking — first() at
994 // logical_start, last() at logical_start + count - 1.
995 let grant = WindowGrant::try_new(1_000, 5, 3, Epoch(7)).unwrap();
996 assert_eq!(grant.physical_ms(), 1_000);
997 assert_eq!(grant.logical_start(), 5);
998 assert_eq!(grant.count(), 3);
999 assert_eq!(grant.epoch(), Epoch(7));
1000 assert_eq!(grant.first(), Timestamp::pack(1_000, 5));
1001 assert_eq!(grant.last(), Timestamp::pack(1_000, 7));
1002 }
1003
1004 #[test]
1005 fn try_new_accepts_max_logical_boundary() {
1006 // logical_start + count - 1 == LOGICAL_MAX is the widest in-range grant.
1007 let grant = WindowGrant::try_new(1_000, 0, LOGICAL_MAX + 1, Epoch(1)).unwrap();
1008 assert_eq!(grant.last(), Timestamp::pack(1_000, LOGICAL_MAX));
1009 }
1010
1011 #[test]
1012 fn try_new_rejects_zero_count() {
1013 // count == 0 would underflow logical_start + count - 1 in last().
1014 assert_eq!(
1015 WindowGrant::try_new(1_000, 0, 0, Epoch(1)),
1016 Err(CoreError::InvalidCount(0))
1017 );
1018 }
1019
1020 #[test]
1021 fn try_new_rejects_out_of_range_physical_ms() {
1022 assert_eq!(
1023 WindowGrant::try_new(PHYSICAL_MS_MAX + 1, 0, 1, Epoch(1)),
1024 Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1))
1025 );
1026 }
1027
1028 #[test]
1029 fn try_new_rejects_logical_range_overflow() {
1030 // last logical (logical_start + count - 1) exceeds the 18-bit field.
1031 assert_eq!(
1032 WindowGrant::try_new(1_000, LOGICAL_MAX, 2, Epoch(1)),
1033 Err(CoreError::LogicalRangeOutOfRange {
1034 logical_start: LOGICAL_MAX,
1035 count: 2
1036 })
1037 );
1038 }
1039
1040 #[test]
1041 fn try_new_rejects_logical_count_u32_overflow() {
1042 // logical_start + (count - 1) overflows u32 before any range check.
1043 assert_eq!(
1044 WindowGrant::try_new(1_000, u32::MAX, 2, Epoch(1)),
1045 Err(CoreError::LogicalRangeOutOfRange {
1046 logical_start: u32::MAX,
1047 count: 2
1048 })
1049 );
1050 }
1051}