Skip to main content

tsoracle_core/
allocator.rs

1//
2//  ░▀█▀░█▀▀░█▀█░█▀▄░█▀█░█▀▀░█░░░█▀▀
3//  ░░█░░▀▀█░█░█░█▀▄░█▀█░█░░░█░░░█▀▀
4//  ░░▀░░▀▀▀░▀▀▀░▀░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀
5//
6//  tsoracle — Distributed Timestamp Oracle
7//
8//  Copyright (c) 2026 Prisma Risk
9//  Licensed under the Apache License, Version 2.0
10//  https://github.com/prisma-risk/tsoracle
11//
12
13// #[PerformanceCriticalPath]
14//! The window allocator state machine. Sync, no I/O.
15
16use crate::{Epoch, LOGICAL_MAX, PHYSICAL_MS_MAX, Timestamp};
17
18#[derive(Copy, Clone, Debug, PartialEq, Eq)]
19pub struct WindowGrant {
20    pub physical_ms: u64,
21    pub logical_start: u32,
22    pub count: u32,
23    pub epoch: Epoch,
24}
25
26impl WindowGrant {
27    pub fn first(&self) -> Timestamp {
28        Timestamp::pack(self.physical_ms, self.logical_start)
29    }
30    pub fn last(&self) -> Timestamp {
31        Timestamp::pack(self.physical_ms, self.logical_start + self.count - 1)
32    }
33}
34
35#[derive(Debug, thiserror::Error, PartialEq, Eq)]
36pub enum CoreError {
37    #[error("not leader")]
38    NotLeader,
39    #[error("window exhausted; caller must extend before retrying")]
40    WindowExhausted,
41    #[error("invalid count: {0}")]
42    InvalidCount(u32),
43    #[error("physical_ms {0} exceeds 46-bit maximum")]
44    PhysicalMsOutOfRange(u64),
45    #[error(
46        "invalid leadership window: fence_floor {fence_floor} exceeds committed_ceiling {committed_ceiling}"
47    )]
48    InvalidLeadershipWindow {
49        fence_floor: u64,
50        committed_ceiling: u64,
51    },
52}
53
54#[derive(Debug)]
55enum State {
56    NotLeader,
57    Leader {
58        epoch: Epoch,
59        /// Persisted upper bound: the allocator will not issue any timestamp with
60        /// `physical_ms` greater than this without a fresh `try_commit_window_extension`.
61        committed_high_water: u64,
62        /// Next `physical_ms` we are willing to issue at. Initialized to
63        /// `fence_floor` on leadership gain, then advances monotonically — never
64        /// retreats below the fence even when `now_ms` is a past value.
65        next_physical_ms: u64,
66        /// Next logical counter within `next_physical_ms`.
67        next_logical: u32,
68    },
69}
70
71pub struct Allocator {
72    state: State,
73}
74
75impl Allocator {
76    pub fn new() -> Self {
77        Allocator {
78            state: State::NotLeader,
79        }
80    }
81
82    /// Seed the allocator once the failover fence has durably persisted both
83    /// the floor and the pre-extended ceiling.
84    ///
85    /// `fence_floor` is the first `physical_ms` the new leader may issue —
86    /// the server sets it to `prior_high_water + 1` so the new leader's
87    /// timestamps are strictly above any the prior leader could have issued.
88    ///
89    /// `committed_ceiling` is the pre-extended upper bound the server has
90    /// already persisted (typically `fence_floor + window_ms`). It must
91    /// satisfy `committed_ceiling >= fence_floor` so the allocator can serve
92    /// `try_grant` immediately without an additional extension round-trip.
93    pub fn try_on_leadership_gained(
94        &mut self,
95        fence_floor: u64,
96        committed_ceiling: u64,
97        epoch: Epoch,
98    ) -> Result<(), CoreError> {
99        if fence_floor > PHYSICAL_MS_MAX {
100            return Err(CoreError::PhysicalMsOutOfRange(fence_floor));
101        }
102        if committed_ceiling > PHYSICAL_MS_MAX {
103            return Err(CoreError::PhysicalMsOutOfRange(committed_ceiling));
104        }
105        if committed_ceiling < fence_floor {
106            return Err(CoreError::InvalidLeadershipWindow {
107                fence_floor,
108                committed_ceiling,
109            });
110        }
111        self.state = State::Leader {
112            epoch,
113            committed_high_water: committed_ceiling,
114            next_physical_ms: fence_floor,
115            next_logical: 0,
116        };
117        Ok(())
118    }
119
120    pub fn on_leadership_lost(&mut self) {
121        self.state = State::NotLeader;
122    }
123
124    pub fn is_leader(&self) -> bool {
125        matches!(self.state, State::Leader { .. })
126    }
127
128    pub fn epoch(&self) -> Option<Epoch> {
129        match self.state {
130            State::Leader { epoch, .. } => Some(epoch),
131            State::NotLeader => None,
132        }
133    }
134
135    /// Hot path. Issue `count` timestamps from the in-memory window.
136    ///
137    /// Returns `WindowExhausted` when the in-memory remainder cannot cover the request;
138    /// the caller (typically the server) then runs prepare → persist → commit and retries.
139    pub fn try_grant(&mut self, now_ms: u64, count: u32) -> Result<WindowGrant, CoreError> {
140        if count == 0 {
141            return Err(CoreError::InvalidCount(0));
142        }
143        if count > LOGICAL_MAX + 1 {
144            return Err(CoreError::InvalidCount(count));
145        }
146        let State::Leader {
147            epoch,
148            committed_high_water,
149            next_physical_ms,
150            next_logical,
151        } = &mut self.state
152        else {
153            return Err(CoreError::NotLeader);
154        };
155
156        // Advance physical_ms toward wall clock if ahead. next_physical_ms is
157        // already at or above fence_floor, so a low now_ms simply leaves it there.
158        if now_ms > *next_physical_ms {
159            *next_physical_ms = now_ms;
160            *next_logical = 0;
161        }
162
163        // If the current physical_ms cannot fit the request in its remaining
164        // logical range, advance to the next physical_ms.
165        if *next_logical as u64 + count as u64 > LOGICAL_MAX as u64 + 1 {
166            *next_physical_ms += 1;
167            *next_logical = 0;
168        }
169
170        if *next_physical_ms > PHYSICAL_MS_MAX {
171            return Err(CoreError::PhysicalMsOutOfRange(*next_physical_ms));
172        }
173
174        // The fence: never issue a timestamp at a physical_ms above the committed
175        // high-water. If we are at or past the bound, the caller must extend.
176        if *next_physical_ms > *committed_high_water {
177            return Err(CoreError::WindowExhausted);
178        }
179
180        let grant = WindowGrant {
181            physical_ms: *next_physical_ms,
182            logical_start: *next_logical,
183            count,
184            epoch: *epoch,
185        };
186        *next_logical += count;
187        Ok(grant)
188    }
189
190    /// Non-mutating predicate: would `try_grant(now_ms, count)` succeed right
191    /// now? Used by the server's extension single-flight to decide whether a
192    /// peer extender has already added enough room, avoiding a redundant
193    /// `persist_high_water` round-trip. Mirrors `try_grant`'s exhaustion check
194    /// exactly — a coarser predicate would risk false positives (skip the
195    /// extension, then fail the outer retry) for requests whose `count`
196    /// straddles the window edge.
197    pub fn would_grant(&self, now_ms: u64, count: u32) -> bool {
198        if count == 0 || count > LOGICAL_MAX + 1 {
199            return false;
200        }
201        let State::Leader {
202            committed_high_water,
203            next_physical_ms,
204            next_logical,
205            ..
206        } = &self.state
207        else {
208            return false;
209        };
210
211        let mut physical_ms = *next_physical_ms;
212        let mut logical = *next_logical;
213        if now_ms > physical_ms {
214            physical_ms = now_ms;
215            logical = 0;
216        }
217        if logical as u64 + count as u64 > LOGICAL_MAX as u64 + 1 {
218            physical_ms += 1;
219        }
220        if physical_ms > PHYSICAL_MS_MAX {
221            return false;
222        }
223        physical_ms <= *committed_high_water
224    }
225
226    /// Compute the high-water value the caller should durably persist before
227    /// calling `try_commit_window_extension`. Does not mutate.
228    ///
229    /// Returns `max(committed_high_water + 1, now_ms) + ahead_ms`. The +1 on
230    /// `committed_high_water` guarantees forward progress when wall clock is
231    /// behind the persisted bound (rare, but possible after a clock-step-back).
232    ///
233    /// Returns `Err(CoreError::NotLeader)` off-leader, matching every other
234    /// mutating method. A `0` sentinel here would be indistinguishable from a
235    /// legitimately prepared bound, letting a caller that skipped `is_leader()`
236    /// proceed as if preparation had succeeded.
237    pub fn try_prepare_window_extension(
238        &self,
239        now_ms: u64,
240        ahead_ms: u64,
241    ) -> Result<u64, CoreError> {
242        match &self.state {
243            State::NotLeader => Err(CoreError::NotLeader),
244            State::Leader {
245                committed_high_water,
246                ..
247            } => {
248                let floor = committed_high_water
249                    .checked_add(1)
250                    .ok_or(CoreError::PhysicalMsOutOfRange(*committed_high_water))?;
251                let requested = core::cmp::max(floor, now_ms)
252                    .checked_add(ahead_ms)
253                    .ok_or(CoreError::PhysicalMsOutOfRange(u64::MAX))?;
254                if requested > PHYSICAL_MS_MAX {
255                    return Err(CoreError::PhysicalMsOutOfRange(requested));
256                }
257                Ok(requested)
258            }
259        }
260    }
261
262    /// Apply a durably-persisted window extension. `persisted_high_water` is
263    /// the value returned by `ConsensusDriver::persist_high_water`, which is
264    /// monotonic — it may equal or exceed the value passed to prepare.
265    ///
266    /// The `expected_epoch` argument fences out late-arriving commits from a
267    /// prior leader epoch: if the allocator is no longer at this epoch (either
268    /// it has lost leadership or a new leader took over), the commit is
269    /// silently dropped. Combined with the server's drain barrier, this
270    /// guarantees a late persist from epoch N cannot raise the durable bound
271    /// observed by epoch N+M.
272    pub fn try_commit_window_extension(
273        &mut self,
274        persisted_high_water: u64,
275        expected_epoch: Epoch,
276    ) -> Result<(), CoreError> {
277        if persisted_high_water > PHYSICAL_MS_MAX {
278            return Err(CoreError::PhysicalMsOutOfRange(persisted_high_water));
279        }
280        if let State::Leader {
281            epoch,
282            committed_high_water,
283            ..
284        } = &mut self.state
285            && *epoch == expected_epoch
286            && persisted_high_water > *committed_high_water
287        {
288            *committed_high_water = persisted_high_water;
289        }
290        Ok(())
291    }
292}
293
294impl Default for Allocator {
295    fn default() -> Self {
296        Self::new()
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    #[test]
305    fn new_allocator_is_not_leader() {
306        let allocator = Allocator::new();
307        assert!(!allocator.is_leader());
308        assert_eq!(allocator.epoch(), None);
309    }
310
311    #[test]
312    fn on_leadership_gained_sets_epoch() {
313        let mut allocator = Allocator::new();
314        allocator
315            .try_on_leadership_gained(1000, 5000, Epoch(5))
316            .unwrap();
317        assert!(allocator.is_leader());
318        assert_eq!(allocator.epoch(), Some(Epoch(5)));
319    }
320
321    #[test]
322    fn try_on_leadership_gained_rejects_out_of_range_window() {
323        let mut allocator = Allocator::new();
324        assert_eq!(
325            allocator.try_on_leadership_gained(PHYSICAL_MS_MAX + 1, PHYSICAL_MS_MAX + 1, Epoch(5)),
326            Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1))
327        );
328        // fence_floor in-range, ceiling out-of-range — separate guard.
329        assert_eq!(
330            allocator.try_on_leadership_gained(1_000, PHYSICAL_MS_MAX + 1, Epoch(5)),
331            Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1))
332        );
333        assert_eq!(
334            allocator.try_on_leadership_gained(5000, 4000, Epoch(5)),
335            Err(CoreError::InvalidLeadershipWindow {
336                fence_floor: 5000,
337                committed_ceiling: 4000
338            })
339        );
340    }
341
342    #[test]
343    fn on_leadership_lost_clears_state() {
344        let mut allocator = Allocator::new();
345        allocator
346            .try_on_leadership_gained(1000, 5000, Epoch(5))
347            .unwrap();
348        allocator.on_leadership_lost();
349        assert!(!allocator.is_leader());
350        assert_eq!(allocator.epoch(), None);
351    }
352
353    #[test]
354    fn try_grant_not_leader() {
355        let mut allocator = Allocator::new();
356        assert_eq!(allocator.try_grant(1000, 1), Err(CoreError::NotLeader));
357    }
358
359    #[test]
360    fn try_grant_zero_count() {
361        let mut allocator = Allocator::new();
362        allocator
363            .try_on_leadership_gained(1000, 5000, Epoch(1))
364            .unwrap();
365        assert_eq!(
366            allocator.try_grant(1000, 0),
367            Err(CoreError::InvalidCount(0))
368        );
369    }
370
371    #[test]
372    fn try_grant_oversized_count() {
373        let mut allocator = Allocator::new();
374        allocator
375            .try_on_leadership_gained(1000, 5000, Epoch(1))
376            .unwrap();
377        let oversized = LOGICAL_MAX + 2;
378        assert_eq!(
379            allocator.try_grant(1000, oversized),
380            Err(CoreError::InvalidCount(oversized))
381        );
382    }
383
384    #[test]
385    fn try_grant_above_committed_is_window_exhausted() {
386        // Advancing `now_ms` past `committed_high_water` correctly returns
387        // WindowExhausted; the server then extends.
388        let mut allocator = Allocator::new();
389        // fence_floor=5_000, ceiling=5_000 (tight window, no pre-extended gap).
390        allocator
391            .try_on_leadership_gained(5_000, 5_000, Epoch(1))
392            .unwrap();
393        // now_ms below floor: clamps to floor=5_000, which equals the ceiling → succeeds.
394        allocator.try_grant(4_999, 1).unwrap();
395        // now_ms above ceiling: window exhausted.
396        assert_eq!(
397            allocator.try_grant(5_001, 1),
398            Err(CoreError::WindowExhausted)
399        );
400    }
401
402    #[test]
403    fn try_grant_after_gain_serves_immediately() {
404        // The fence has already persisted a pre-extended window, so the allocator
405        // can serve immediately. Grants start at fence_floor regardless of now_ms.
406        let mut allocator = Allocator::new();
407        allocator
408            .try_on_leadership_gained(5_000, 10_000, Epoch(1))
409            .unwrap();
410        let grant = allocator.try_grant(1_000, 1).unwrap();
411        // now_ms=1_000 < fence_floor=5_000, so next_physical_ms stays at 5_000.
412        assert_eq!(grant.physical_ms, 5_000);
413        assert_eq!(grant.logical_start, 0);
414        assert_eq!(grant.epoch, Epoch(1));
415    }
416
417    #[test]
418    fn prepare_window_extension_not_leader() {
419        // Off-leader prepare must error like every other mutating method, not
420        // return a `0` that a caller could mistake for a prepared bound.
421        let allocator = Allocator::new();
422        assert_eq!(
423            allocator.try_prepare_window_extension(1000, 3000),
424            Err(CoreError::NotLeader)
425        );
426    }
427
428    #[test]
429    fn prepare_window_extension_uses_now_ms_when_ahead_of_high_water() {
430        let mut allocator = Allocator::new();
431        allocator
432            .try_on_leadership_gained(1000, 1000, Epoch(1))
433            .unwrap();
434        let target = allocator.try_prepare_window_extension(2000, 3000).unwrap();
435        assert_eq!(target, 5000); // max(1001, 2000) + 3000
436    }
437
438    #[test]
439    fn prepare_window_extension_uses_high_water_floor_when_clock_behind() {
440        let mut allocator = Allocator::new();
441        allocator
442            .try_on_leadership_gained(10_000, 10_000, Epoch(1))
443            .unwrap();
444        let target = allocator.try_prepare_window_extension(500, 3000).unwrap();
445        // floor = 10_001, clock = 500. max = 10_001. + 3000 = 13_001.
446        assert_eq!(target, 13_001);
447    }
448
449    #[test]
450    fn prepare_window_extension_rejects_out_of_range_target() {
451        let mut allocator = Allocator::new();
452        allocator
453            .try_on_leadership_gained(PHYSICAL_MS_MAX, PHYSICAL_MS_MAX, Epoch(1))
454            .unwrap();
455        assert_eq!(
456            allocator.try_prepare_window_extension(PHYSICAL_MS_MAX, 1),
457            Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 2))
458        );
459    }
460
461    #[test]
462    fn commit_then_try_grant_succeeds() {
463        let mut allocator = Allocator::new();
464        allocator
465            .try_on_leadership_gained(1000, 1000, Epoch(7))
466            .unwrap();
467        let target = allocator.try_prepare_window_extension(1000, 3000).unwrap();
468        allocator
469            .try_commit_window_extension(target, Epoch(7))
470            .unwrap();
471        let grant = allocator.try_grant(1000, 5).unwrap();
472        assert_eq!(grant.count, 5);
473        assert_eq!(grant.logical_start, 0);
474        assert_eq!(grant.epoch, Epoch(7));
475        // physical_ms should be at most the persisted high-water.
476        assert!(grant.physical_ms <= target);
477    }
478
479    #[test]
480    fn commit_with_lower_value_is_ignored() {
481        let mut allocator = Allocator::new();
482        allocator
483            .try_on_leadership_gained(1000, 1000, Epoch(1))
484            .unwrap();
485        allocator
486            .try_commit_window_extension(5000, Epoch(1))
487            .unwrap();
488        allocator
489            .try_commit_window_extension(3000, Epoch(1))
490            .unwrap(); // attempt to regress
491        // try_grant up to physical_ms=5000 should still work.
492        let grant = allocator.try_grant(4500, 1).unwrap();
493        assert_eq!(grant.physical_ms, 4500);
494    }
495
496    #[test]
497    fn commit_rejects_out_of_range_high_water() {
498        let mut allocator = Allocator::new();
499        allocator
500            .try_on_leadership_gained(1000, 1000, Epoch(1))
501            .unwrap();
502        assert_eq!(
503            allocator.try_commit_window_extension(PHYSICAL_MS_MAX + 1, Epoch(1)),
504            Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1))
505        );
506    }
507
508    #[test]
509    fn try_grant_rejects_out_of_range_clock() {
510        let mut allocator = Allocator::new();
511        allocator
512            .try_on_leadership_gained(1000, PHYSICAL_MS_MAX, Epoch(1))
513            .unwrap();
514        assert_eq!(
515            allocator.try_grant(PHYSICAL_MS_MAX + 1, 1),
516            Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1))
517        );
518    }
519
520    #[test]
521    fn commit_at_wrong_epoch_is_silently_dropped() {
522        let mut allocator = Allocator::new();
523        // fence_floor=1000, ceiling=1000: tight initial window.
524        allocator
525            .try_on_leadership_gained(1000, 1000, Epoch(5))
526            .unwrap();
527        // A late persist from epoch 4 (the prior leader) — fenced out.
528        allocator
529            .try_commit_window_extension(9_999, Epoch(4))
530            .unwrap();
531        // The allocator's bound did not move; a grant at now=900 clamps to
532        // floor=1000, and a request with now=1_100 exhausts the window.
533        allocator.try_grant(900, 1).unwrap();
534        assert_eq!(
535            allocator.try_grant(1_100, 1),
536            Err(CoreError::WindowExhausted)
537        );
538    }
539
540    #[test]
541    fn commit_after_leadership_lost_is_ignored() {
542        let mut allocator = Allocator::new();
543        allocator
544            .try_on_leadership_gained(1000, 5000, Epoch(1))
545            .unwrap();
546        allocator.on_leadership_lost();
547        allocator
548            .try_commit_window_extension(9_999, Epoch(1))
549            .unwrap();
550        assert!(!allocator.is_leader());
551    }
552
553    #[test]
554    fn would_grant_matches_try_grant_outcome() {
555        let mut allocator = Allocator::new();
556        // Not leader: never grants.
557        assert!(!allocator.would_grant(1_000, 1));
558        // Invalid counts: never grants.
559        allocator
560            .try_on_leadership_gained(1_000, 5_000, Epoch(1))
561            .unwrap();
562        assert!(!allocator.would_grant(1_000, 0));
563        assert!(!allocator.would_grant(1_000, LOGICAL_MAX + 2));
564        // Within-window: matches try_grant. now_ms below floor still grants
565        // (clamped to floor=1_000, ceiling=5_000).
566        assert!(allocator.would_grant(0, 1));
567        // now_ms above ceiling: predicate refuses (would exhaust).
568        assert!(!allocator.would_grant(5_001, 1));
569        // Mid-window now_ms advances the predicate's internal physical_ms.
570        assert!(allocator.would_grant(2_500, 1));
571    }
572
573    #[test]
574    fn would_grant_predicts_logical_wrap_advance() {
575        // When (logical + count) overflows the per-ms logical range, the
576        // predicate (like try_grant) advances physical_ms by 1. If that
577        // advance leaves the window, would_grant must return false.
578        let mut allocator = Allocator::new();
579        allocator
580            .try_on_leadership_gained(1_000, 1_000, Epoch(1))
581            .unwrap();
582        // count >= LOGICAL_MAX + 1 forces the advance branch on a fresh
583        // window: logical(0) + count(LOGICAL_MAX+1) doesn't overflow on its
584        // own, but anything one bigger does. Use LOGICAL_MAX + 1 to land at
585        // the edge, then any non-zero issue advances physical_ms.
586        allocator.try_grant(1_000, LOGICAL_MAX + 1).unwrap();
587        // Next grant of size 1 would advance to physical_ms = 1_001, which
588        // exceeds the committed ceiling of 1_000.
589        assert!(!allocator.would_grant(1_000, 1));
590    }
591
592    #[test]
593    fn would_grant_returns_false_when_advance_exceeds_physical_max() {
594        // Construct an allocator at PHYSICAL_MS_MAX so the +1 advance
595        // crosses the 46-bit ceiling and the predicate refuses.
596        let mut allocator = Allocator::new();
597        allocator
598            .try_on_leadership_gained(PHYSICAL_MS_MAX, PHYSICAL_MS_MAX, Epoch(1))
599            .unwrap();
600        // Fill the logical range so the next would_grant call has to
601        // advance physical_ms.
602        allocator
603            .try_grant(PHYSICAL_MS_MAX, LOGICAL_MAX + 1)
604            .unwrap();
605        assert!(!allocator.would_grant(PHYSICAL_MS_MAX, 1));
606    }
607
608    #[test]
609    fn default_constructs_not_leader_allocator() {
610        let allocator = Allocator::default();
611        assert!(!allocator.is_leader());
612        assert_eq!(allocator.epoch(), None);
613    }
614
615    #[test]
616    fn logical_wraps_to_next_physical_ms() {
617        let mut allocator = Allocator::new();
618        // fence_floor=0, ceiling=0; extend to 10 before granting.
619        allocator.try_on_leadership_gained(0, 0, Epoch(1)).unwrap();
620        allocator.try_commit_window_extension(10, Epoch(1)).unwrap();
621        // Issue LOGICAL_MAX+1 logicals at physical_ms=1, then one more should bump to 2.
622        let first = allocator.try_grant(1, LOGICAL_MAX + 1).unwrap();
623        assert_eq!(first.physical_ms, 1);
624        assert_eq!(first.logical_start, 0);
625        let second = allocator.try_grant(1, 1).unwrap();
626        assert_eq!(second.physical_ms, 2);
627        assert_eq!(second.logical_start, 0);
628    }
629}