Skip to main content

tsoracle_core/
seq.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//! Keyed dense sequence types: validated keys, contiguous grants, and the
25//! leadership/epoch gate. Pure and synchronous, no I/O — the same discipline as
26//! `allocator.rs`. Unlike `Allocator`, this holds NO per-key counter state: every
27//! counter lives in the durable layer and every block `start` is assigned there.
28
29use crate::{CoreError, Epoch};
30
31/// Maximum length, in bytes, of a sequence key's UTF-8 encoding.
32pub const MAX_SEQ_KEY_LEN: usize = 128;
33
34/// Default per-call ceiling on `GetSeq`'s `count` — the largest block a single
35/// request may reserve when the server has not overridden it.
36///
37/// Unlike the timestamp path's `MAX_TIMESTAMPS_PER_RPC` (forced by the 18-bit
38/// logical field in the packed wire format), nothing in the dense format
39/// requires a particular ceiling: `start` is `u64`, the wire `count` is `u32`,
40/// and the durable counter is `u64`. This cap is a soft anti-abuse guardrail —
41/// a dense block is permanently consumed (the gapless counter only moves
42/// forward), so an unbounded `count` would let one call irrevocably burn a huge
43/// span of a key's namespace. The value (`2^16`) is a deliberate round default;
44/// operators tune it via [`ServerBuilder::max_seq_count`].
45///
46/// [`ServerBuilder::max_seq_count`]: https://docs.rs/tsoracle-server
47pub const DEFAULT_MAX_SEQ_COUNT: u32 = 65_536;
48
49/// A validated sequence key: non-empty, valid UTF-8 (guaranteed by `String`),
50/// and at most [`MAX_SEQ_KEY_LEN`] bytes. `try_new` is the single validation
51/// site — a value of this type is proof the key is in range.
52#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
53#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
54pub struct SeqKey(String);
55
56impl SeqKey {
57    pub fn try_new(s: impl Into<String>) -> Result<Self, CoreError> {
58        let s = s.into();
59        if s.is_empty() {
60            return Err(CoreError::SeqKeyEmpty);
61        }
62        if s.len() > MAX_SEQ_KEY_LEN {
63            return Err(CoreError::SeqKeyTooLong {
64                len: s.len(),
65                max: MAX_SEQ_KEY_LEN,
66            });
67        }
68        Ok(SeqKey(s))
69    }
70
71    pub fn as_str(&self) -> &str {
72        &self.0
73    }
74}
75
76/// A contiguous block of `count` dense ordinals for one key, starting at
77/// `start`, issued under one leadership `epoch`. Covers `[start, start + count)`.
78///
79/// The only constructor, [`try_new`](Self::try_new), validates that the block is
80/// non-empty (`count >= 1`) and that its last ordinal `start + count - 1` does
81/// not overflow `u64`. A constructed value is therefore proof that
82/// [`last`](Self::last) neither underflows nor wraps — the invariant lives in the
83/// type, not in whatever code happened to build it. Mirrors [`WindowGrant`].
84///
85/// [`WindowGrant`]: crate::WindowGrant
86#[derive(Clone, Debug, PartialEq, Eq)]
87pub struct SeqGrant {
88    key: SeqKey,
89    start: u64,
90    count: u32,
91    epoch: Epoch,
92}
93
94impl SeqGrant {
95    /// Construct a grant, validating that [`last`](Self::last) is infallible.
96    ///
97    /// Rejects `count == 0` ([`CoreError::SeqCountZero`]) — a block covers at
98    /// least one ordinal, and `last`'s `start + count - 1` would underflow — and
99    /// a block whose last ordinal `start + count - 1` exceeds `u64::MAX`
100    /// ([`CoreError::SeqBlockOverflow`]). In the server's serving path neither
101    /// can occur (`count` is validated `1..=max_seq_count` and the durable
102    /// fetch-add already rejected an overflowing advance), but `SeqGrant` is
103    /// public, so the constructor enforces the invariant for every caller.
104    pub fn try_new(key: SeqKey, start: u64, count: u32, epoch: Epoch) -> Result<Self, CoreError> {
105        if count == 0 {
106            return Err(CoreError::SeqCountZero);
107        }
108        // last() = start + count - 1; reject inputs where that would wrap.
109        if start.checked_add(u64::from(count) - 1).is_none() {
110            return Err(CoreError::SeqBlockOverflow { start, count });
111        }
112        Ok(SeqGrant {
113            key,
114            start,
115            count,
116            epoch,
117        })
118    }
119    pub fn key(&self) -> &SeqKey {
120        &self.key
121    }
122    pub fn start(&self) -> u64 {
123        self.start
124    }
125    pub fn count(&self) -> u32 {
126        self.count
127    }
128    pub fn epoch(&self) -> Epoch {
129        self.epoch
130    }
131    /// The last ordinal in the block: `start + count - 1`. Infallible:
132    /// [`try_new`](Self::try_new) validated `count >= 1` (so `count - 1` does not
133    /// underflow) and that `start + (count - 1)` fits in `u64`, so a constructed
134    /// `SeqGrant` witnesses this cannot panic. The `start + (count - 1)`
135    /// association matches that check exactly — computing `(start + count) - 1`
136    /// would overflow the intermediate at the `last() == u64::MAX` boundary even
137    /// though the result fits.
138    pub fn last(&self) -> u64 {
139        self.start + (u64::from(self.count) - 1)
140    }
141}
142
143#[derive(Debug)]
144enum SeqState {
145    NotLeader,
146    Leader { epoch: Epoch },
147}
148
149/// Leadership/epoch gate plus request validation for the dense path. Holds NO
150/// per-key counter state — counters live in the durable layer and `start` is
151/// assigned there. This type only decides "may this request proceed, and is it
152/// well-formed?".
153pub struct SeqAllocator {
154    state: SeqState,
155}
156
157impl SeqAllocator {
158    pub fn new() -> Self {
159        SeqAllocator {
160            state: SeqState::NotLeader,
161        }
162    }
163
164    /// Transition to leader state for `epoch`. The caller must already hold
165    /// consensus leadership for `epoch` before calling this.
166    pub fn become_leader(&mut self, epoch: Epoch) {
167        self.state = SeqState::Leader { epoch };
168    }
169
170    pub fn step_down(&mut self) {
171        self.state = SeqState::NotLeader;
172    }
173
174    pub fn is_leader(&self) -> bool {
175        matches!(self.state, SeqState::Leader { .. })
176    }
177
178    pub fn epoch(&self) -> Option<Epoch> {
179        match self.state {
180            SeqState::Leader { epoch } => Some(epoch),
181            SeqState::NotLeader => None,
182        }
183    }
184
185    /// Validate a request without touching durable state. Leadership is checked
186    /// first; then count bounds (zero, then oversized), then key validity.
187    /// Returns the validated [`SeqKey`].
188    ///
189    /// `max_count` is the caller's per-call ceiling — the server's configured
190    /// [`ServerBuilder::max_seq_count`], which defaults to
191    /// [`DEFAULT_MAX_SEQ_COUNT`]. Injecting it (rather than reading the constant
192    /// here) keeps the cap a server-side policy the operator can tune, while the
193    /// `count >= 1` floor stays a hard invariant enforced unconditionally.
194    ///
195    /// [`ServerBuilder::max_seq_count`]: https://docs.rs/tsoracle-server
196    pub fn validate_request(
197        &self,
198        key: &str,
199        count: u32,
200        max_count: u32,
201    ) -> Result<SeqKey, CoreError> {
202        if !self.is_leader() {
203            return Err(CoreError::NotLeader);
204        }
205        if count == 0 {
206            return Err(CoreError::SeqCountZero);
207        }
208        if count > max_count {
209            return Err(CoreError::SeqCountTooLarge {
210                count,
211                max: max_count,
212            });
213        }
214        SeqKey::try_new(key)
215    }
216}
217
218impl Default for SeqAllocator {
219    fn default() -> Self {
220        Self::new()
221    }
222}
223
224#[cfg(test)]
225mod seqallocator_tests {
226    use super::*;
227
228    #[test]
229    fn new_is_not_leader() {
230        let a = SeqAllocator::new();
231        assert!(!a.is_leader());
232        assert_eq!(a.epoch(), None);
233    }
234
235    #[test]
236    fn become_leader_then_step_down() {
237        let mut a = SeqAllocator::new();
238        a.become_leader(Epoch(3));
239        assert!(a.is_leader());
240        assert_eq!(a.epoch(), Some(Epoch(3)));
241        a.step_down();
242        assert!(!a.is_leader());
243        assert_eq!(a.epoch(), None);
244    }
245
246    #[test]
247    fn validate_request_off_leader_is_not_leader() {
248        let a = SeqAllocator::new();
249        assert_eq!(
250            a.validate_request("orders", 1, DEFAULT_MAX_SEQ_COUNT),
251            Err(CoreError::NotLeader)
252        );
253    }
254
255    #[test]
256    fn validate_request_rejects_zero_count() {
257        let mut a = SeqAllocator::new();
258        a.become_leader(Epoch(1));
259        assert_eq!(
260            a.validate_request("orders", 0, DEFAULT_MAX_SEQ_COUNT),
261            Err(CoreError::SeqCountZero)
262        );
263    }
264
265    #[test]
266    fn validate_request_rejects_oversized_count() {
267        let mut a = SeqAllocator::new();
268        a.become_leader(Epoch(1));
269        assert_eq!(
270            a.validate_request("orders", DEFAULT_MAX_SEQ_COUNT + 1, DEFAULT_MAX_SEQ_COUNT),
271            Err(CoreError::SeqCountTooLarge {
272                count: DEFAULT_MAX_SEQ_COUNT + 1,
273                max: DEFAULT_MAX_SEQ_COUNT
274            })
275        );
276    }
277
278    #[test]
279    fn validate_request_uses_caller_provided_max() {
280        // The ceiling is the `max_count` argument, not the default constant: a
281        // smaller cap rejects below the default, and a larger cap accepts above
282        // it — proving the cap is injected policy, not a hard-coded limit.
283        let mut a = SeqAllocator::new();
284        a.become_leader(Epoch(1));
285        assert_eq!(
286            a.validate_request("orders", 11, 10),
287            Err(CoreError::SeqCountTooLarge { count: 11, max: 10 }),
288            "count above a small configured cap must be rejected",
289        );
290        assert!(
291            a.validate_request("orders", 10, 10).is_ok(),
292            "count at the configured cap must be accepted",
293        );
294        assert!(
295            a.validate_request(
296                "orders",
297                DEFAULT_MAX_SEQ_COUNT + 1,
298                DEFAULT_MAX_SEQ_COUNT * 2
299            )
300            .is_ok(),
301            "a larger configured cap must accept counts above the default",
302        );
303    }
304
305    #[test]
306    fn validate_request_rejects_bad_key() {
307        let mut a = SeqAllocator::new();
308        a.become_leader(Epoch(1));
309        assert_eq!(
310            a.validate_request("", 1, DEFAULT_MAX_SEQ_COUNT),
311            Err(CoreError::SeqKeyEmpty)
312        );
313    }
314
315    #[test]
316    fn validate_request_ok_returns_key() {
317        let mut a = SeqAllocator::new();
318        a.become_leader(Epoch(1));
319        let k = a
320            .validate_request("orders", 10, DEFAULT_MAX_SEQ_COUNT)
321            .unwrap();
322        assert_eq!(k.as_str(), "orders");
323    }
324
325    #[test]
326    fn validate_request_accepts_max_count_exactly() {
327        let mut a = SeqAllocator::new();
328        a.become_leader(Epoch(1));
329        assert!(
330            a.validate_request("orders", DEFAULT_MAX_SEQ_COUNT, DEFAULT_MAX_SEQ_COUNT)
331                .is_ok()
332        );
333    }
334}
335
336#[cfg(test)]
337mod seqgrant_tests {
338    use super::*;
339
340    #[test]
341    fn exposes_fields_and_last() {
342        let key = SeqKey::try_new("users").unwrap();
343        let g = SeqGrant::try_new(key.clone(), 100, 5, Epoch(7)).unwrap();
344        assert_eq!(g.key().as_str(), "users");
345        assert_eq!(g.start(), 100);
346        assert_eq!(g.count(), 5);
347        assert_eq!(g.epoch(), Epoch(7));
348        // [100, 105): last issued ordinal is 104.
349        assert_eq!(g.last(), 104);
350    }
351
352    #[test]
353    fn last_equals_start_when_count_is_one() {
354        let g1 = SeqGrant::try_new(SeqKey::try_new("x").unwrap(), 42, 1, Epoch(1)).unwrap();
355        assert_eq!(g1.last(), 42);
356    }
357
358    #[test]
359    fn try_new_rejects_zero_count() {
360        let key = SeqKey::try_new("x").unwrap();
361        assert_eq!(
362            SeqGrant::try_new(key, 5, 0, Epoch(1)),
363            Err(CoreError::SeqCountZero)
364        );
365    }
366
367    #[test]
368    fn try_new_rejects_block_overflow() {
369        // start + count - 1 overflows u64, which would make `last()` wrap.
370        let key = SeqKey::try_new("x").unwrap();
371        assert_eq!(
372            SeqGrant::try_new(key, u64::MAX, 2, Epoch(1)),
373            Err(CoreError::SeqBlockOverflow {
374                start: u64::MAX,
375                count: 2
376            })
377        );
378    }
379
380    #[test]
381    fn try_new_accepts_max_boundary() {
382        // start + count - 1 == u64::MAX exactly is the largest valid block.
383        let key = SeqKey::try_new("x").unwrap();
384        let g = SeqGrant::try_new(key, u64::MAX - 4, 5, Epoch(1)).unwrap();
385        assert_eq!(g.last(), u64::MAX);
386    }
387}
388
389#[cfg(test)]
390mod seqkey_tests {
391    use super::*;
392
393    #[test]
394    fn accepts_normal_key() {
395        let k = SeqKey::try_new("orders").unwrap();
396        assert_eq!(k.as_str(), "orders");
397    }
398
399    #[test]
400    fn rejects_empty() {
401        assert_eq!(SeqKey::try_new(""), Err(CoreError::SeqKeyEmpty));
402    }
403
404    #[test]
405    fn accepts_max_length() {
406        let s = "a".repeat(MAX_SEQ_KEY_LEN);
407        assert!(SeqKey::try_new(&s).is_ok());
408    }
409
410    #[test]
411    fn rejects_one_past_max_length() {
412        let s = "a".repeat(MAX_SEQ_KEY_LEN + 1);
413        assert_eq!(
414            SeqKey::try_new(&s),
415            Err(CoreError::SeqKeyTooLong {
416                len: MAX_SEQ_KEY_LEN + 1,
417                max: MAX_SEQ_KEY_LEN
418            })
419        );
420    }
421
422    #[test]
423    fn length_is_measured_in_utf8_bytes_not_chars() {
424        // 'é' is 2 bytes; 64 of them = 128 bytes = exactly the cap.
425        let ok = "é".repeat(MAX_SEQ_KEY_LEN / 2);
426        assert!(SeqKey::try_new(&ok).is_ok());
427        let too_long = "é".repeat(MAX_SEQ_KEY_LEN / 2 + 1);
428        assert!(matches!(
429            SeqKey::try_new(&too_long),
430            Err(CoreError::SeqKeyTooLong { .. })
431        ));
432    }
433}