Skip to main content

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    /// Shared count guard for `try_grant` and `would_grant`, kept here so the
266    /// two entry points cannot drift. The server's extension single-flight
267    /// relies on `would_grant(now_ms, count) == true` implying the retry
268    /// `try_grant(now_ms, count)` succeeds (see `service::extend_window`), so
269    /// the set of rejected counts must be identical on both paths — splitting
270    /// this check across both methods would let a future edit to one degrade
271    /// the recheck into a spurious consensus round-trip (or worse).
272    ///
273    /// `count == 0` reuses the oversized case's `InvalidCount(count)`; since
274    /// `count` is `0` there, the surfaced value matches the prior explicit
275    /// `InvalidCount(0)`.
276    fn validate_count(count: u32) -> Result<(), CoreError> {
277        if count == 0 || count > LOGICAL_MAX + 1 {
278            return Err(CoreError::InvalidCount(count));
279        }
280        Ok(())
281    }
282
283    /// Single source of truth for the window-advance simulation and its bounds
284    /// checks, shared by `try_grant` and `would_grant`. Pure: it takes the
285    /// relevant state fields by value and mutates nothing, so a failed
286    /// projection cannot leave allocator state advanced.
287    ///
288    /// On success returns the `(physical_ms, logical_start)` the grant would
289    /// occupy. The two failure variants are kept distinct so `try_grant` can
290    /// surface the precise error its callers (and tests) expect;
291    /// `would_grant` collapses both to `false` via `.is_ok()`.
292    fn project_grant(
293        next_physical_ms: u64,
294        next_logical: u32,
295        committed_high_water: u64,
296        now_ms: u64,
297        count: u32,
298    ) -> Result<(u64, u32), CoreError> {
299        let mut physical_ms = next_physical_ms;
300        let mut logical = next_logical;
301
302        // Advance physical_ms toward wall clock if ahead. next_physical_ms is
303        // already at or above fence_floor, so a low now_ms simply leaves it there.
304        if now_ms > physical_ms {
305            physical_ms = now_ms;
306            logical = 0;
307        }
308
309        // If the current physical_ms cannot fit the request in its remaining
310        // logical range, advance to the next physical_ms.
311        if logical as u64 + count as u64 > LOGICAL_MAX as u64 + 1 {
312            physical_ms += 1;
313            logical = 0;
314        }
315
316        if physical_ms > PHYSICAL_MS_MAX {
317            return Err(CoreError::PhysicalMsOutOfRange(physical_ms));
318        }
319
320        // The fence: never issue a timestamp at a physical_ms above the committed
321        // high-water. If we are at or past the bound, the caller must extend.
322        if physical_ms > committed_high_water {
323            return Err(CoreError::WindowExhausted);
324        }
325
326        Ok((physical_ms, logical))
327    }
328
329    /// Hot path. Issue `count` timestamps from the in-memory window.
330    ///
331    /// Returns `WindowExhausted` when the in-memory remainder cannot cover the request;
332    /// the caller (typically the server) then runs prepare → persist → commit and retries.
333    ///
334    /// State is written back only on success: a failed grant (out-of-range or
335    /// exhausted window) leaves `next_physical_ms`/`next_logical` untouched.
336    pub fn try_grant(&mut self, now_ms: u64, count: u32) -> Result<WindowGrant, CoreError> {
337        Self::validate_count(count)?;
338        let State::Leader {
339            epoch,
340            committed_high_water,
341            next_physical_ms,
342            next_logical,
343        } = &mut self.state
344        else {
345            return Err(CoreError::NotLeader);
346        };
347
348        let (physical_ms, logical_start) = Self::project_grant(
349            *next_physical_ms,
350            *next_logical,
351            *committed_high_water,
352            now_ms,
353            count,
354        )?;
355
356        // project_grant already guarantees physical_ms and the logical range
357        // are in bounds, so this construction never fails in practice — but
358        // routing through the checked constructor propagates a CoreError rather
359        // than panicking should that invariant ever be violated, and keeps this
360        // path free of the unwrap/expect the crate's panic policy bans.
361        let grant = WindowGrant::try_new(physical_ms, logical_start, count, *epoch)?;
362        // Normalize the write-back so the stored cursor is always a packable
363        // (physical_ms, logical) pair. `project_grant` admits a grant that fills
364        // the millisecond exactly, so `logical_start + count` can reach
365        // LOGICAL_MAX + 1 — a logical the packed layout cannot hold. Rather than
366        // store that sentinel and rely on the next `project_grant` call to wrap
367        // it, roll to the next millisecond at logical 0 here. This is precisely
368        // the position the next call would compute, so behavior is unchanged;
369        // the stored state just no longer depends on the implicit
370        // "next call always wraps or resets" invariant. (`next_physical_ms` may
371        // become PHYSICAL_MS_MAX + 1, which is never packed directly and is
372        // rejected as out-of-range by the next grant's `project_grant`.)
373        let next_logical_after = logical_start + count;
374        if next_logical_after > LOGICAL_MAX {
375            *next_physical_ms = physical_ms + 1;
376            *next_logical = 0;
377        } else {
378            *next_physical_ms = physical_ms;
379            *next_logical = next_logical_after;
380        }
381        Ok(grant)
382    }
383
384    /// Non-mutating predicate: would `try_grant(now_ms, count)` succeed right
385    /// now? Used by the server's extension single-flight to decide whether a
386    /// peer extender has already added enough room, avoiding a redundant
387    /// `persist_high_water` round-trip. Delegates to the same `project_grant`
388    /// helper `try_grant` uses, so the exhaustion check cannot drift — a
389    /// coarser predicate would risk false positives (skip the extension, then
390    /// fail the outer retry) for requests whose `count` straddles the window edge.
391    pub fn would_grant(&self, now_ms: u64, count: u32) -> bool {
392        if Self::validate_count(count).is_err() {
393            return false;
394        }
395        let State::Leader {
396            committed_high_water,
397            next_physical_ms,
398            next_logical,
399            ..
400        } = &self.state
401        else {
402            return false;
403        };
404
405        Self::project_grant(
406            *next_physical_ms,
407            *next_logical,
408            *committed_high_water,
409            now_ms,
410            count,
411        )
412        .is_ok()
413    }
414
415    /// Compute the high-water value the caller should durably persist before
416    /// calling `try_commit_window_extension`. Does not mutate.
417    ///
418    /// Returns `max(committed_high_water + 1, now_ms) + ahead_ms`. The +1 on
419    /// `committed_high_water` guarantees forward progress when wall clock is
420    /// behind the persisted bound (rare, but possible after a clock-step-back).
421    ///
422    /// Returns `Err(CoreError::NotLeader)` off-leader, matching every other
423    /// mutating method. A `0` sentinel here would be indistinguishable from a
424    /// legitimately prepared bound, letting a caller that skipped `is_leader()`
425    /// proceed as if preparation had succeeded.
426    pub fn try_prepare_window_extension(
427        &self,
428        now_ms: u64,
429        ahead_ms: u64,
430    ) -> Result<u64, CoreError> {
431        match &self.state {
432            State::NotLeader => Err(CoreError::NotLeader),
433            State::Leader {
434                committed_high_water,
435                ..
436            } => {
437                let floor = committed_high_water
438                    .checked_add(1)
439                    .ok_or(CoreError::PhysicalMsOutOfRange(*committed_high_water))?;
440                let requested = core::cmp::max(floor, now_ms).checked_add(ahead_ms).ok_or(
441                    CoreError::WindowExtensionOverflow {
442                        floor,
443                        now_ms,
444                        ahead_ms,
445                    },
446                )?;
447                if requested > PHYSICAL_MS_MAX {
448                    return Err(CoreError::PhysicalMsOutOfRange(requested));
449                }
450                Ok(requested)
451            }
452        }
453    }
454
455    /// Apply a durably-persisted window extension. `persisted_high_water` is
456    /// the value returned by `ConsensusDriver::persist_high_water`, which is
457    /// monotonic — it may equal or exceed the value passed to prepare.
458    ///
459    /// The `expected_epoch` argument fences out late-arriving commits from a
460    /// prior leader epoch: if the allocator is no longer at this epoch (either
461    /// it has lost leadership or a new leader took over), the commit is
462    /// dropped. Combined with the server's drain barrier, this guarantees a
463    /// late persist from epoch N cannot raise the durable bound observed by
464    /// epoch N+M.
465    ///
466    /// Returns [`CommitOutcome`]: `Applied` when the bound advanced, or
467    /// `Ignored` (with the reason) for the three benign drop cases. A value
468    /// exceeding the 46-bit physical ceiling is an invariant violation, not a
469    /// benign drop, so it stays `Err(PhysicalMsOutOfRange)`.
470    pub fn try_commit_window_extension(
471        &mut self,
472        persisted_high_water: u64,
473        expected_epoch: Epoch,
474    ) -> Result<CommitOutcome, CoreError> {
475        if persisted_high_water > PHYSICAL_MS_MAX {
476            return Err(CoreError::PhysicalMsOutOfRange(persisted_high_water));
477        }
478        let State::Leader {
479            epoch,
480            committed_high_water,
481            ..
482        } = &mut self.state
483        else {
484            return Ok(CommitOutcome::Ignored(IgnoreReason::NotLeader));
485        };
486        // Epoch fencing takes precedence over the monotonic check: a late
487        // persist from a superseded epoch must report EpochMismatch even when
488        // its value also fails to advance, so churn is not masked as reordering.
489        if *epoch != expected_epoch {
490            return Ok(CommitOutcome::Ignored(IgnoreReason::EpochMismatch {
491                expected: expected_epoch,
492                current: *epoch,
493            }));
494        }
495        if persisted_high_water <= *committed_high_water {
496            return Ok(CommitOutcome::Ignored(IgnoreReason::NotAdvanced {
497                persisted: persisted_high_water,
498                committed: *committed_high_water,
499            }));
500        }
501        *committed_high_water = persisted_high_water;
502        Ok(CommitOutcome::Applied(persisted_high_water))
503    }
504}
505
506impl Default for Allocator {
507    fn default() -> Self {
508        Self::new()
509    }
510}
511
512#[cfg(test)]
513mod tests {
514    use super::*;
515
516    #[test]
517    fn new_allocator_is_not_leader() {
518        let allocator = Allocator::new();
519        assert!(!allocator.is_leader());
520        assert_eq!(allocator.epoch(), None);
521    }
522
523    #[test]
524    fn on_leadership_gained_sets_epoch() {
525        let mut allocator = Allocator::new();
526        allocator
527            .try_on_leadership_gained(1000, 5000, Epoch(5))
528            .unwrap();
529        assert!(allocator.is_leader());
530        assert_eq!(allocator.epoch(), Some(Epoch(5)));
531    }
532
533    #[test]
534    fn try_on_leadership_gained_rejects_out_of_range_window() {
535        let mut allocator = Allocator::new();
536        assert_eq!(
537            allocator.try_on_leadership_gained(PHYSICAL_MS_MAX + 1, PHYSICAL_MS_MAX + 1, Epoch(5)),
538            Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1))
539        );
540        // fence_floor in-range, ceiling out-of-range — separate guard.
541        assert_eq!(
542            allocator.try_on_leadership_gained(1_000, PHYSICAL_MS_MAX + 1, Epoch(5)),
543            Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1))
544        );
545        assert_eq!(
546            allocator.try_on_leadership_gained(5000, 4000, Epoch(5)),
547            Err(CoreError::InvalidLeadershipWindow {
548                fence_floor: 5000,
549                committed_ceiling: 4000
550            })
551        );
552    }
553
554    #[test]
555    fn on_leadership_lost_clears_state() {
556        let mut allocator = Allocator::new();
557        allocator
558            .try_on_leadership_gained(1000, 5000, Epoch(5))
559            .unwrap();
560        allocator.on_leadership_lost();
561        assert!(!allocator.is_leader());
562        assert_eq!(allocator.epoch(), None);
563    }
564
565    #[test]
566    fn try_grant_not_leader() {
567        let mut allocator = Allocator::new();
568        assert_eq!(allocator.try_grant(1000, 1), Err(CoreError::NotLeader));
569    }
570
571    #[test]
572    fn try_grant_zero_count() {
573        let mut allocator = Allocator::new();
574        allocator
575            .try_on_leadership_gained(1000, 5000, Epoch(1))
576            .unwrap();
577        assert_eq!(
578            allocator.try_grant(1000, 0),
579            Err(CoreError::InvalidCount(0))
580        );
581    }
582
583    #[test]
584    fn try_grant_oversized_count() {
585        let mut allocator = Allocator::new();
586        allocator
587            .try_on_leadership_gained(1000, 5000, Epoch(1))
588            .unwrap();
589        let oversized = LOGICAL_MAX + 2;
590        assert_eq!(
591            allocator.try_grant(1000, oversized),
592            Err(CoreError::InvalidCount(oversized))
593        );
594    }
595
596    #[test]
597    fn try_grant_above_committed_is_window_exhausted() {
598        // Advancing `now_ms` past `committed_high_water` correctly returns
599        // WindowExhausted; the server then extends.
600        let mut allocator = Allocator::new();
601        // fence_floor=5_000, ceiling=5_000 (tight window, no pre-extended gap).
602        allocator
603            .try_on_leadership_gained(5_000, 5_000, Epoch(1))
604            .unwrap();
605        // now_ms below floor: clamps to floor=5_000, which equals the ceiling → succeeds.
606        allocator.try_grant(4_999, 1).unwrap();
607        // now_ms above ceiling: window exhausted.
608        assert_eq!(
609            allocator.try_grant(5_001, 1),
610            Err(CoreError::WindowExhausted)
611        );
612    }
613
614    #[test]
615    fn failed_try_grant_does_not_advance_state() {
616        // A grant that fails the exhaustion check must leave the allocator's
617        // advance state untouched, so a later grant at a lower `now_ms` is not
618        // pinned to the failed attempt's wall clock.
619        let mut allocator = Allocator::new();
620        // Tight initial window: fence_floor == ceiling == 1_000.
621        allocator
622            .try_on_leadership_gained(1_000, 1_000, Epoch(1))
623            .unwrap();
624        // now_ms far past the ceiling exhausts the window.
625        assert_eq!(
626            allocator.try_grant(5_000, 1),
627            Err(CoreError::WindowExhausted)
628        );
629        // Extend the durable bound to exactly 2_000.
630        let target = allocator.try_prepare_window_extension(2_000, 0).unwrap();
631        assert_eq!(target, 2_000); // max(committed+1=1_001, now=2_000) + 0
632        allocator
633            .try_commit_window_extension(target, Epoch(1))
634            .unwrap();
635        // The failed grant must not have pinned next_physical_ms at 5_000: a
636        // grant at now_ms=2_000 advances cleanly to physical_ms=2_000 (<= the
637        // committed 2_000). If state had advanced on the failure, next_physical_ms
638        // would still be 5_000 and this would exhaust the window again.
639        let grant = allocator.try_grant(2_000, 1).unwrap();
640        assert_eq!(grant.physical_ms, 2_000);
641        assert_eq!(grant.logical_start, 0);
642    }
643
644    #[test]
645    fn try_grant_after_gain_serves_immediately() {
646        // The fence has already persisted a pre-extended window, so the allocator
647        // can serve immediately. Grants start at fence_floor regardless of now_ms.
648        let mut allocator = Allocator::new();
649        allocator
650            .try_on_leadership_gained(5_000, 10_000, Epoch(1))
651            .unwrap();
652        let grant = allocator.try_grant(1_000, 1).unwrap();
653        // now_ms=1_000 < fence_floor=5_000, so next_physical_ms stays at 5_000.
654        assert_eq!(grant.physical_ms, 5_000);
655        assert_eq!(grant.logical_start, 0);
656        assert_eq!(grant.epoch, Epoch(1));
657    }
658
659    #[test]
660    fn prepare_window_extension_not_leader() {
661        // Off-leader prepare must error like every other mutating method, not
662        // return a `0` that a caller could mistake for a prepared bound.
663        let allocator = Allocator::new();
664        assert_eq!(
665            allocator.try_prepare_window_extension(1000, 3000),
666            Err(CoreError::NotLeader)
667        );
668    }
669
670    #[test]
671    fn prepare_window_extension_uses_now_ms_when_ahead_of_high_water() {
672        let mut allocator = Allocator::new();
673        allocator
674            .try_on_leadership_gained(1000, 1000, Epoch(1))
675            .unwrap();
676        let target = allocator.try_prepare_window_extension(2000, 3000).unwrap();
677        assert_eq!(target, 5000); // max(1001, 2000) + 3000
678    }
679
680    #[test]
681    fn prepare_window_extension_uses_high_water_floor_when_clock_behind() {
682        let mut allocator = Allocator::new();
683        allocator
684            .try_on_leadership_gained(10_000, 10_000, Epoch(1))
685            .unwrap();
686        let target = allocator.try_prepare_window_extension(500, 3000).unwrap();
687        // floor = 10_001, clock = 500. max = 10_001. + 3000 = 13_001.
688        assert_eq!(target, 13_001);
689    }
690
691    #[test]
692    fn prepare_window_extension_rejects_out_of_range_target() {
693        let mut allocator = Allocator::new();
694        allocator
695            .try_on_leadership_gained(PHYSICAL_MS_MAX, PHYSICAL_MS_MAX, Epoch(1))
696            .unwrap();
697        assert_eq!(
698            allocator.try_prepare_window_extension(PHYSICAL_MS_MAX, 1),
699            Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 2))
700        );
701    }
702
703    #[test]
704    fn prepare_window_extension_overflow_names_all_operands() {
705        // A saturated clock (SystemClock::now_ms saturates to u64::MAX) plus any
706        // non-zero ahead_ms overflows max(floor, now_ms) + ahead_ms. The error
707        // must name the three real operands so the log points at the clock, not
708        // a phantom "someone passed an absurd physical_ms".
709        let mut allocator = Allocator::new();
710        allocator
711            .try_on_leadership_gained(1_000, 1_000, Epoch(1))
712            .unwrap();
713        assert_eq!(
714            allocator.try_prepare_window_extension(u64::MAX, 1),
715            Err(CoreError::WindowExtensionOverflow {
716                floor: 1_001,
717                now_ms: u64::MAX,
718                ahead_ms: 1,
719            })
720        );
721    }
722
723    #[test]
724    fn commit_then_try_grant_succeeds() {
725        let mut allocator = Allocator::new();
726        allocator
727            .try_on_leadership_gained(1000, 1000, Epoch(7))
728            .unwrap();
729        let target = allocator.try_prepare_window_extension(1000, 3000).unwrap();
730        assert_eq!(
731            allocator.try_commit_window_extension(target, Epoch(7)),
732            Ok(CommitOutcome::Applied(target))
733        );
734        let grant = allocator.try_grant(1000, 5).unwrap();
735        assert_eq!(grant.count, 5);
736        assert_eq!(grant.logical_start, 0);
737        assert_eq!(grant.epoch, Epoch(7));
738        // physical_ms should be at most the persisted high-water.
739        assert!(grant.physical_ms <= target);
740    }
741
742    #[test]
743    fn commit_with_lower_value_is_ignored() {
744        let mut allocator = Allocator::new();
745        allocator
746            .try_on_leadership_gained(1000, 1000, Epoch(1))
747            .unwrap();
748        assert_eq!(
749            allocator.try_commit_window_extension(5000, Epoch(1)),
750            Ok(CommitOutcome::Applied(5000))
751        );
752        // A non-advancing commit reports the values so the caller can tell a
753        // monotonic-bound regression (persist reordering) from epoch churn.
754        assert_eq!(
755            allocator.try_commit_window_extension(3000, Epoch(1)),
756            Ok(CommitOutcome::Ignored(IgnoreReason::NotAdvanced {
757                persisted: 3000,
758                committed: 5000,
759            }))
760        );
761        // try_grant up to physical_ms=5000 should still work.
762        let grant = allocator.try_grant(4500, 1).unwrap();
763        assert_eq!(grant.physical_ms, 4500);
764    }
765
766    #[test]
767    fn commit_with_equal_value_is_ignored_not_applied() {
768        // persist_high_water is monotonic and may *equal* the prepared bound; an
769        // equal value moves nothing, so it is Ignored(NotAdvanced), not Applied.
770        let mut allocator = Allocator::new();
771        allocator
772            .try_on_leadership_gained(1000, 5000, Epoch(1))
773            .unwrap();
774        assert_eq!(
775            allocator.try_commit_window_extension(5000, Epoch(1)),
776            Ok(CommitOutcome::Ignored(IgnoreReason::NotAdvanced {
777                persisted: 5000,
778                committed: 5000,
779            }))
780        );
781    }
782
783    #[test]
784    fn commit_rejects_out_of_range_high_water() {
785        let mut allocator = Allocator::new();
786        allocator
787            .try_on_leadership_gained(1000, 1000, Epoch(1))
788            .unwrap();
789        assert_eq!(
790            allocator.try_commit_window_extension(PHYSICAL_MS_MAX + 1, Epoch(1)),
791            Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1))
792        );
793    }
794
795    #[test]
796    fn try_grant_rejects_out_of_range_clock() {
797        let mut allocator = Allocator::new();
798        allocator
799            .try_on_leadership_gained(1000, PHYSICAL_MS_MAX, Epoch(1))
800            .unwrap();
801        assert_eq!(
802            allocator.try_grant(PHYSICAL_MS_MAX + 1, 1),
803            Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1))
804        );
805    }
806
807    #[test]
808    fn commit_at_wrong_epoch_is_silently_dropped() {
809        let mut allocator = Allocator::new();
810        // fence_floor=1000, ceiling=1000: tight initial window.
811        allocator
812            .try_on_leadership_gained(1000, 1000, Epoch(5))
813            .unwrap();
814        // A late persist from epoch 4 (the prior leader) — fenced out. The
815        // outcome names both epochs so the caller can metric epoch churn.
816        assert_eq!(
817            allocator.try_commit_window_extension(9_999, Epoch(4)),
818            Ok(CommitOutcome::Ignored(IgnoreReason::EpochMismatch {
819                expected: Epoch(4),
820                current: Epoch(5),
821            }))
822        );
823        // The allocator's bound did not move; a grant at now=900 clamps to
824        // floor=1000, and a request with now=1_100 exhausts the window.
825        allocator.try_grant(900, 1).unwrap();
826        assert_eq!(
827            allocator.try_grant(1_100, 1),
828            Err(CoreError::WindowExhausted)
829        );
830    }
831
832    #[test]
833    fn commit_after_leadership_lost_is_ignored() {
834        let mut allocator = Allocator::new();
835        allocator
836            .try_on_leadership_gained(1000, 5000, Epoch(1))
837            .unwrap();
838        allocator.on_leadership_lost();
839        assert_eq!(
840            allocator.try_commit_window_extension(9_999, Epoch(1)),
841            Ok(CommitOutcome::Ignored(IgnoreReason::NotLeader))
842        );
843        assert!(!allocator.is_leader());
844    }
845
846    #[test]
847    fn would_grant_matches_try_grant_outcome() {
848        let mut allocator = Allocator::new();
849        // Not leader: never grants.
850        assert!(!allocator.would_grant(1_000, 1));
851        // Invalid counts: never grants.
852        allocator
853            .try_on_leadership_gained(1_000, 5_000, Epoch(1))
854            .unwrap();
855        assert!(!allocator.would_grant(1_000, 0));
856        assert!(!allocator.would_grant(1_000, LOGICAL_MAX + 2));
857        // Within-window: matches try_grant. now_ms below floor still grants
858        // (clamped to floor=1_000, ceiling=5_000).
859        assert!(allocator.would_grant(0, 1));
860        // now_ms above ceiling: predicate refuses (would exhaust).
861        assert!(!allocator.would_grant(5_001, 1));
862        // Mid-window now_ms advances the predicate's internal physical_ms.
863        assert!(allocator.would_grant(2_500, 1));
864    }
865
866    #[test]
867    fn would_grant_predicts_logical_wrap_advance() {
868        // When (logical + count) overflows the per-ms logical range, the
869        // predicate (like try_grant) advances physical_ms by 1. If that
870        // advance leaves the window, would_grant must return false.
871        let mut allocator = Allocator::new();
872        allocator
873            .try_on_leadership_gained(1_000, 1_000, Epoch(1))
874            .unwrap();
875        // count >= LOGICAL_MAX + 1 forces the advance branch on a fresh
876        // window: logical(0) + count(LOGICAL_MAX+1) doesn't overflow on its
877        // own, but anything one bigger does. Use LOGICAL_MAX + 1 to land at
878        // the edge, then any non-zero issue advances physical_ms.
879        allocator.try_grant(1_000, LOGICAL_MAX + 1).unwrap();
880        // Next grant of size 1 would advance to physical_ms = 1_001, which
881        // exceeds the committed ceiling of 1_000.
882        assert!(!allocator.would_grant(1_000, 1));
883    }
884
885    #[test]
886    fn would_grant_returns_false_when_advance_exceeds_physical_max() {
887        // Construct an allocator at PHYSICAL_MS_MAX so the +1 advance
888        // crosses the 46-bit ceiling and the predicate refuses.
889        let mut allocator = Allocator::new();
890        allocator
891            .try_on_leadership_gained(PHYSICAL_MS_MAX, PHYSICAL_MS_MAX, Epoch(1))
892            .unwrap();
893        // Fill the logical range so the next would_grant call has to
894        // advance physical_ms.
895        allocator
896            .try_grant(PHYSICAL_MS_MAX, LOGICAL_MAX + 1)
897            .unwrap();
898        assert!(!allocator.would_grant(PHYSICAL_MS_MAX, 1));
899    }
900
901    #[test]
902    fn default_constructs_not_leader_allocator() {
903        let allocator = Allocator::default();
904        assert!(!allocator.is_leader());
905        assert_eq!(allocator.epoch(), None);
906    }
907
908    #[test]
909    fn logical_wraps_to_next_physical_ms() {
910        let mut allocator = Allocator::new();
911        // fence_floor=0, ceiling=0; extend to 10 before granting.
912        allocator.try_on_leadership_gained(0, 0, Epoch(1)).unwrap();
913        allocator.try_commit_window_extension(10, Epoch(1)).unwrap();
914        // Issue LOGICAL_MAX+1 logicals at physical_ms=1, then one more should bump to 2.
915        let first = allocator.try_grant(1, LOGICAL_MAX + 1).unwrap();
916        assert_eq!(first.physical_ms, 1);
917        assert_eq!(first.logical_start, 0);
918        let second = allocator.try_grant(1, 1).unwrap();
919        assert_eq!(second.physical_ms, 2);
920        assert_eq!(second.logical_start, 0);
921    }
922
923    #[test]
924    fn exact_fill_grant_normalizes_stored_state_to_packable() {
925        // A grant that consumes a millisecond's entire logical range
926        // (logical_start + count == LOGICAL_MAX + 1) must not leave the
927        // LOGICAL_MAX+1 sentinel in next_logical: that value cannot be packed
928        // (Timestamp::pack asserts logical <= LOGICAL_MAX), so the stored state
929        // would only be safe by the implicit "next call always wraps" invariant.
930        // The write-back normalizes it to the already-rolled position
931        // (physical_ms + 1, 0), so stored state is always directly packable.
932        let mut allocator = Allocator::new();
933        allocator.try_on_leadership_gained(0, 0, Epoch(1)).unwrap();
934        allocator.try_commit_window_extension(10, Epoch(1)).unwrap();
935        // Fill physical_ms=1 exactly: logical [0, LOGICAL_MAX].
936        let grant = allocator.try_grant(1, LOGICAL_MAX + 1).unwrap();
937        assert_eq!(grant.physical_ms, 1);
938        assert_eq!(grant.logical_start, 0);
939
940        let State::Leader {
941            next_physical_ms,
942            next_logical,
943            ..
944        } = allocator.state
945        else {
946            panic!("expected leader state after a successful grant");
947        };
948        // The stored cursor rolled to the next millisecond at logical 0 …
949        assert_eq!(next_physical_ms, 2);
950        assert_eq!(next_logical, 0);
951        // … and is, by construction, a packable timestamp.
952        assert!(Timestamp::try_pack(next_physical_ms, next_logical).is_ok());
953    }
954
955    #[test]
956    fn try_new_accepts_valid_grant_and_packs_boundaries() {
957        // A checked grant exposes its fields through accessors and its
958        // boundary timestamps pack without panicking — first() at
959        // logical_start, last() at logical_start + count - 1.
960        let grant = WindowGrant::try_new(1_000, 5, 3, Epoch(7)).unwrap();
961        assert_eq!(grant.physical_ms(), 1_000);
962        assert_eq!(grant.logical_start(), 5);
963        assert_eq!(grant.count(), 3);
964        assert_eq!(grant.epoch(), Epoch(7));
965        assert_eq!(grant.first(), Timestamp::pack(1_000, 5));
966        assert_eq!(grant.last(), Timestamp::pack(1_000, 7));
967    }
968
969    #[test]
970    fn try_new_accepts_max_logical_boundary() {
971        // logical_start + count - 1 == LOGICAL_MAX is the widest in-range grant.
972        let grant = WindowGrant::try_new(1_000, 0, LOGICAL_MAX + 1, Epoch(1)).unwrap();
973        assert_eq!(grant.last(), Timestamp::pack(1_000, LOGICAL_MAX));
974    }
975
976    #[test]
977    fn try_new_rejects_zero_count() {
978        // count == 0 would underflow logical_start + count - 1 in last().
979        assert_eq!(
980            WindowGrant::try_new(1_000, 0, 0, Epoch(1)),
981            Err(CoreError::InvalidCount(0))
982        );
983    }
984
985    #[test]
986    fn try_new_rejects_out_of_range_physical_ms() {
987        assert_eq!(
988            WindowGrant::try_new(PHYSICAL_MS_MAX + 1, 0, 1, Epoch(1)),
989            Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1))
990        );
991    }
992
993    #[test]
994    fn try_new_rejects_logical_range_overflow() {
995        // last logical (logical_start + count - 1) exceeds the 18-bit field.
996        assert_eq!(
997            WindowGrant::try_new(1_000, LOGICAL_MAX, 2, Epoch(1)),
998            Err(CoreError::LogicalRangeOutOfRange {
999                logical_start: LOGICAL_MAX,
1000                count: 2
1001            })
1002        );
1003    }
1004
1005    #[test]
1006    fn try_new_rejects_logical_count_u32_overflow() {
1007        // logical_start + (count - 1) overflows u32 before any range check.
1008        assert_eq!(
1009            WindowGrant::try_new(1_000, u32::MAX, 2, Epoch(1)),
1010            Err(CoreError::LogicalRangeOutOfRange {
1011                logical_start: u32::MAX,
1012                count: 2
1013            })
1014        );
1015    }
1016}