Skip to main content

tsoracle_core/
allocator.rs

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