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    pub fn try_prepare_window_extension(
233        &self,
234        now_ms: u64,
235        ahead_ms: u64,
236    ) -> Result<u64, CoreError> {
237        match &self.state {
238            State::NotLeader => Ok(0),
239            State::Leader {
240                committed_high_water,
241                ..
242            } => {
243                let floor = committed_high_water
244                    .checked_add(1)
245                    .ok_or(CoreError::PhysicalMsOutOfRange(*committed_high_water))?;
246                let requested = core::cmp::max(floor, now_ms)
247                    .checked_add(ahead_ms)
248                    .ok_or(CoreError::PhysicalMsOutOfRange(u64::MAX))?;
249                if requested > PHYSICAL_MS_MAX {
250                    return Err(CoreError::PhysicalMsOutOfRange(requested));
251                }
252                Ok(requested)
253            }
254        }
255    }
256
257    /// Apply a durably-persisted window extension. `persisted_high_water` is
258    /// the value returned by `ConsensusDriver::persist_high_water`, which is
259    /// monotonic — it may equal or exceed the value passed to prepare.
260    ///
261    /// The `expected_epoch` argument fences out late-arriving commits from a
262    /// prior leader epoch: if the allocator is no longer at this epoch (either
263    /// it has lost leadership or a new leader took over), the commit is
264    /// silently dropped. Combined with the server's drain barrier, this
265    /// guarantees a late persist from epoch N cannot raise the durable bound
266    /// observed by epoch N+M.
267    pub fn try_commit_window_extension(
268        &mut self,
269        persisted_high_water: u64,
270        expected_epoch: Epoch,
271    ) -> Result<(), CoreError> {
272        if persisted_high_water > PHYSICAL_MS_MAX {
273            return Err(CoreError::PhysicalMsOutOfRange(persisted_high_water));
274        }
275        if let State::Leader {
276            epoch,
277            committed_high_water,
278            ..
279        } = &mut self.state
280            && *epoch == expected_epoch
281            && persisted_high_water > *committed_high_water
282        {
283            *committed_high_water = persisted_high_water;
284        }
285        Ok(())
286    }
287}
288
289impl Default for Allocator {
290    fn default() -> Self {
291        Self::new()
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298
299    #[test]
300    fn new_allocator_is_not_leader() {
301        let allocator = Allocator::new();
302        assert!(!allocator.is_leader());
303        assert_eq!(allocator.epoch(), None);
304    }
305
306    #[test]
307    fn on_leadership_gained_sets_epoch() {
308        let mut allocator = Allocator::new();
309        allocator
310            .try_on_leadership_gained(1000, 5000, Epoch(5))
311            .unwrap();
312        assert!(allocator.is_leader());
313        assert_eq!(allocator.epoch(), Some(Epoch(5)));
314    }
315
316    #[test]
317    fn try_on_leadership_gained_rejects_out_of_range_window() {
318        let mut allocator = Allocator::new();
319        assert_eq!(
320            allocator.try_on_leadership_gained(PHYSICAL_MS_MAX + 1, PHYSICAL_MS_MAX + 1, Epoch(5)),
321            Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1))
322        );
323        // fence_floor in-range, ceiling out-of-range — separate guard.
324        assert_eq!(
325            allocator.try_on_leadership_gained(1_000, PHYSICAL_MS_MAX + 1, Epoch(5)),
326            Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1))
327        );
328        assert_eq!(
329            allocator.try_on_leadership_gained(5000, 4000, Epoch(5)),
330            Err(CoreError::InvalidLeadershipWindow {
331                fence_floor: 5000,
332                committed_ceiling: 4000
333            })
334        );
335    }
336
337    #[test]
338    fn on_leadership_lost_clears_state() {
339        let mut allocator = Allocator::new();
340        allocator
341            .try_on_leadership_gained(1000, 5000, Epoch(5))
342            .unwrap();
343        allocator.on_leadership_lost();
344        assert!(!allocator.is_leader());
345        assert_eq!(allocator.epoch(), None);
346    }
347
348    #[test]
349    fn try_grant_not_leader() {
350        let mut allocator = Allocator::new();
351        assert_eq!(allocator.try_grant(1000, 1), Err(CoreError::NotLeader));
352    }
353
354    #[test]
355    fn try_grant_zero_count() {
356        let mut allocator = Allocator::new();
357        allocator
358            .try_on_leadership_gained(1000, 5000, Epoch(1))
359            .unwrap();
360        assert_eq!(
361            allocator.try_grant(1000, 0),
362            Err(CoreError::InvalidCount(0))
363        );
364    }
365
366    #[test]
367    fn try_grant_oversized_count() {
368        let mut allocator = Allocator::new();
369        allocator
370            .try_on_leadership_gained(1000, 5000, Epoch(1))
371            .unwrap();
372        let oversized = LOGICAL_MAX + 2;
373        assert_eq!(
374            allocator.try_grant(1000, oversized),
375            Err(CoreError::InvalidCount(oversized))
376        );
377    }
378
379    #[test]
380    fn try_grant_above_committed_is_window_exhausted() {
381        // Advancing `now_ms` past `committed_high_water` correctly returns
382        // WindowExhausted; the server then extends.
383        let mut allocator = Allocator::new();
384        // fence_floor=5_000, ceiling=5_000 (tight window, no pre-extended gap).
385        allocator
386            .try_on_leadership_gained(5_000, 5_000, Epoch(1))
387            .unwrap();
388        // now_ms below floor: clamps to floor=5_000, which equals the ceiling → succeeds.
389        allocator.try_grant(4_999, 1).unwrap();
390        // now_ms above ceiling: window exhausted.
391        assert_eq!(
392            allocator.try_grant(5_001, 1),
393            Err(CoreError::WindowExhausted)
394        );
395    }
396
397    #[test]
398    fn try_grant_after_gain_serves_immediately() {
399        // The fence has already persisted a pre-extended window, so the allocator
400        // can serve immediately. Grants start at fence_floor regardless of now_ms.
401        let mut allocator = Allocator::new();
402        allocator
403            .try_on_leadership_gained(5_000, 10_000, Epoch(1))
404            .unwrap();
405        let grant = allocator.try_grant(1_000, 1).unwrap();
406        // now_ms=1_000 < fence_floor=5_000, so next_physical_ms stays at 5_000.
407        assert_eq!(grant.physical_ms, 5_000);
408        assert_eq!(grant.logical_start, 0);
409        assert_eq!(grant.epoch, Epoch(1));
410    }
411
412    #[test]
413    fn prepare_window_extension_uses_now_ms_when_ahead_of_high_water() {
414        let mut allocator = Allocator::new();
415        allocator
416            .try_on_leadership_gained(1000, 1000, Epoch(1))
417            .unwrap();
418        let target = allocator.try_prepare_window_extension(2000, 3000).unwrap();
419        assert_eq!(target, 5000); // max(1001, 2000) + 3000
420    }
421
422    #[test]
423    fn prepare_window_extension_uses_high_water_floor_when_clock_behind() {
424        let mut allocator = Allocator::new();
425        allocator
426            .try_on_leadership_gained(10_000, 10_000, Epoch(1))
427            .unwrap();
428        let target = allocator.try_prepare_window_extension(500, 3000).unwrap();
429        // floor = 10_001, clock = 500. max = 10_001. + 3000 = 13_001.
430        assert_eq!(target, 13_001);
431    }
432
433    #[test]
434    fn prepare_window_extension_rejects_out_of_range_target() {
435        let mut allocator = Allocator::new();
436        allocator
437            .try_on_leadership_gained(PHYSICAL_MS_MAX, PHYSICAL_MS_MAX, Epoch(1))
438            .unwrap();
439        assert_eq!(
440            allocator.try_prepare_window_extension(PHYSICAL_MS_MAX, 1),
441            Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 2))
442        );
443    }
444
445    #[test]
446    fn commit_then_try_grant_succeeds() {
447        let mut allocator = Allocator::new();
448        allocator
449            .try_on_leadership_gained(1000, 1000, Epoch(7))
450            .unwrap();
451        let target = allocator.try_prepare_window_extension(1000, 3000).unwrap();
452        allocator
453            .try_commit_window_extension(target, Epoch(7))
454            .unwrap();
455        let grant = allocator.try_grant(1000, 5).unwrap();
456        assert_eq!(grant.count, 5);
457        assert_eq!(grant.logical_start, 0);
458        assert_eq!(grant.epoch, Epoch(7));
459        // physical_ms should be at most the persisted high-water.
460        assert!(grant.physical_ms <= target);
461    }
462
463    #[test]
464    fn commit_with_lower_value_is_ignored() {
465        let mut allocator = Allocator::new();
466        allocator
467            .try_on_leadership_gained(1000, 1000, Epoch(1))
468            .unwrap();
469        allocator
470            .try_commit_window_extension(5000, Epoch(1))
471            .unwrap();
472        allocator
473            .try_commit_window_extension(3000, Epoch(1))
474            .unwrap(); // attempt to regress
475        // try_grant up to physical_ms=5000 should still work.
476        let grant = allocator.try_grant(4500, 1).unwrap();
477        assert_eq!(grant.physical_ms, 4500);
478    }
479
480    #[test]
481    fn commit_rejects_out_of_range_high_water() {
482        let mut allocator = Allocator::new();
483        allocator
484            .try_on_leadership_gained(1000, 1000, Epoch(1))
485            .unwrap();
486        assert_eq!(
487            allocator.try_commit_window_extension(PHYSICAL_MS_MAX + 1, Epoch(1)),
488            Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1))
489        );
490    }
491
492    #[test]
493    fn try_grant_rejects_out_of_range_clock() {
494        let mut allocator = Allocator::new();
495        allocator
496            .try_on_leadership_gained(1000, PHYSICAL_MS_MAX, Epoch(1))
497            .unwrap();
498        assert_eq!(
499            allocator.try_grant(PHYSICAL_MS_MAX + 1, 1),
500            Err(CoreError::PhysicalMsOutOfRange(PHYSICAL_MS_MAX + 1))
501        );
502    }
503
504    #[test]
505    fn commit_at_wrong_epoch_is_silently_dropped() {
506        let mut allocator = Allocator::new();
507        // fence_floor=1000, ceiling=1000: tight initial window.
508        allocator
509            .try_on_leadership_gained(1000, 1000, Epoch(5))
510            .unwrap();
511        // A late persist from epoch 4 (the prior leader) — fenced out.
512        allocator
513            .try_commit_window_extension(9_999, Epoch(4))
514            .unwrap();
515        // The allocator's bound did not move; a grant at now=900 clamps to
516        // floor=1000, and a request with now=1_100 exhausts the window.
517        allocator.try_grant(900, 1).unwrap();
518        assert_eq!(
519            allocator.try_grant(1_100, 1),
520            Err(CoreError::WindowExhausted)
521        );
522    }
523
524    #[test]
525    fn commit_after_leadership_lost_is_ignored() {
526        let mut allocator = Allocator::new();
527        allocator
528            .try_on_leadership_gained(1000, 5000, Epoch(1))
529            .unwrap();
530        allocator.on_leadership_lost();
531        allocator
532            .try_commit_window_extension(9_999, Epoch(1))
533            .unwrap();
534        assert!(!allocator.is_leader());
535    }
536
537    #[test]
538    fn would_grant_matches_try_grant_outcome() {
539        let mut allocator = Allocator::new();
540        // Not leader: never grants.
541        assert!(!allocator.would_grant(1_000, 1));
542        // Invalid counts: never grants.
543        allocator
544            .try_on_leadership_gained(1_000, 5_000, Epoch(1))
545            .unwrap();
546        assert!(!allocator.would_grant(1_000, 0));
547        assert!(!allocator.would_grant(1_000, LOGICAL_MAX + 2));
548        // Within-window: matches try_grant. now_ms below floor still grants
549        // (clamped to floor=1_000, ceiling=5_000).
550        assert!(allocator.would_grant(0, 1));
551        // now_ms above ceiling: predicate refuses (would exhaust).
552        assert!(!allocator.would_grant(5_001, 1));
553        // Mid-window now_ms advances the predicate's internal physical_ms.
554        assert!(allocator.would_grant(2_500, 1));
555    }
556
557    #[test]
558    fn would_grant_predicts_logical_wrap_advance() {
559        // When (logical + count) overflows the per-ms logical range, the
560        // predicate (like try_grant) advances physical_ms by 1. If that
561        // advance leaves the window, would_grant must return false.
562        let mut allocator = Allocator::new();
563        allocator
564            .try_on_leadership_gained(1_000, 1_000, Epoch(1))
565            .unwrap();
566        // count >= LOGICAL_MAX + 1 forces the advance branch on a fresh
567        // window: logical(0) + count(LOGICAL_MAX+1) doesn't overflow on its
568        // own, but anything one bigger does. Use LOGICAL_MAX + 1 to land at
569        // the edge, then any non-zero issue advances physical_ms.
570        allocator.try_grant(1_000, LOGICAL_MAX + 1).unwrap();
571        // Next grant of size 1 would advance to physical_ms = 1_001, which
572        // exceeds the committed ceiling of 1_000.
573        assert!(!allocator.would_grant(1_000, 1));
574    }
575
576    #[test]
577    fn would_grant_returns_false_when_advance_exceeds_physical_max() {
578        // Construct an allocator at PHYSICAL_MS_MAX so the +1 advance
579        // crosses the 46-bit ceiling and the predicate refuses.
580        let mut allocator = Allocator::new();
581        allocator
582            .try_on_leadership_gained(PHYSICAL_MS_MAX, PHYSICAL_MS_MAX, Epoch(1))
583            .unwrap();
584        // Fill the logical range so the next would_grant call has to
585        // advance physical_ms.
586        allocator
587            .try_grant(PHYSICAL_MS_MAX, LOGICAL_MAX + 1)
588            .unwrap();
589        assert!(!allocator.would_grant(PHYSICAL_MS_MAX, 1));
590    }
591
592    #[test]
593    fn default_constructs_not_leader_allocator() {
594        let allocator = Allocator::default();
595        assert!(!allocator.is_leader());
596        assert_eq!(allocator.epoch(), None);
597    }
598
599    #[test]
600    fn logical_wraps_to_next_physical_ms() {
601        let mut allocator = Allocator::new();
602        // fence_floor=0, ceiling=0; extend to 10 before granting.
603        allocator.try_on_leadership_gained(0, 0, Epoch(1)).unwrap();
604        allocator.try_commit_window_extension(10, Epoch(1)).unwrap();
605        // Issue LOGICAL_MAX+1 logicals at physical_ms=1, then one more should bump to 2.
606        let first = allocator.try_grant(1, LOGICAL_MAX + 1).unwrap();
607        assert_eq!(first.physical_ms, 1);
608        assert_eq!(first.logical_start, 0);
609        let second = allocator.try_grant(1, 1).unwrap();
610        assert_eq!(second.physical_ms, 2);
611        assert_eq!(second.logical_start, 0);
612    }
613}