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///
53/// The `serde` impls are hand-written (not derived) so deserialization routes
54/// through `try_new`: a derived `Deserialize` for a newtype builds the inner
55/// `String` directly and would be a second construction site that bypasses the
56/// invariant. Mirrors [`crate::peer::PeerEndpoint`]'s validate-on-deserialize
57/// discipline. `Serialize` emits the bare string (newtype-transparent), so the
58/// on-disk and wire byte layout is unchanged.
59#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
60pub struct SeqKey(String);
61
62impl SeqKey {
63    pub fn try_new(s: impl Into<String>) -> Result<Self, CoreError> {
64        let s = s.into();
65        if s.is_empty() {
66            return Err(CoreError::SeqKeyEmpty);
67        }
68        if s.len() > MAX_SEQ_KEY_LEN {
69            return Err(CoreError::SeqKeyTooLong {
70                len: s.len(),
71                max: MAX_SEQ_KEY_LEN,
72            });
73        }
74        Ok(SeqKey(s))
75    }
76
77    pub fn as_str(&self) -> &str {
78        &self.0
79    }
80}
81
82#[cfg(feature = "serde")]
83impl serde::Serialize for SeqKey {
84    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
85        serializer.serialize_str(&self.0)
86    }
87}
88
89#[cfg(feature = "serde")]
90impl<'de> serde::Deserialize<'de> for SeqKey {
91    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
92        // Re-validate on the way in: `try_new` is the single validation site, so
93        // a crafted or corrupted key (empty, or longer than `MAX_SEQ_KEY_LEN`
94        // bytes) arriving in a replicated log entry or a persisted snapshot is
95        // rejected here rather than silently observed as an in-range `SeqKey`.
96        let s = String::deserialize(deserializer)?;
97        SeqKey::try_new(s).map_err(serde::de::Error::custom)
98    }
99}
100
101/// A contiguous block of `count` dense ordinals for one key, starting at
102/// `start`, issued under one leadership `epoch`. Covers `[start, start + count)`.
103///
104/// The only constructor, [`try_new`](Self::try_new), validates that the block is
105/// non-empty (`count >= 1`) and that its last ordinal `start + count - 1` does
106/// not overflow `u64`. A constructed value is therefore proof that
107/// [`last`](Self::last) neither underflows nor wraps — the invariant lives in the
108/// type, not in whatever code happened to build it. Mirrors [`WindowGrant`].
109///
110/// [`WindowGrant`]: crate::WindowGrant
111#[derive(Clone, Debug, PartialEq, Eq)]
112pub struct SeqGrant {
113    key: SeqKey,
114    start: u64,
115    count: u32,
116    epoch: Epoch,
117}
118
119impl SeqGrant {
120    /// Construct a grant, validating that [`last`](Self::last) is infallible.
121    ///
122    /// Rejects `count == 0` ([`CoreError::SeqCountZero`]) — a block covers at
123    /// least one ordinal, and `last`'s `start + count - 1` would underflow — and
124    /// a block whose last ordinal `start + count - 1` exceeds `u64::MAX`
125    /// ([`CoreError::SeqBlockOverflow`]). In the server's serving path neither
126    /// can occur (`count` is validated `1..=max_seq_count` and the durable
127    /// fetch-add already rejected an overflowing advance), but `SeqGrant` is
128    /// public, so the constructor enforces the invariant for every caller.
129    pub fn try_new(key: SeqKey, start: u64, count: u32, epoch: Epoch) -> Result<Self, CoreError> {
130        if count == 0 {
131            return Err(CoreError::SeqCountZero);
132        }
133        // last() = start + count - 1; reject inputs where that would wrap.
134        if start.checked_add(u64::from(count) - 1).is_none() {
135            return Err(CoreError::SeqBlockOverflow { start, count });
136        }
137        Ok(SeqGrant {
138            key,
139            start,
140            count,
141            epoch,
142        })
143    }
144    pub fn key(&self) -> &SeqKey {
145        &self.key
146    }
147    pub fn start(&self) -> u64 {
148        self.start
149    }
150    pub fn count(&self) -> u32 {
151        self.count
152    }
153    pub fn epoch(&self) -> Epoch {
154        self.epoch
155    }
156    /// The last ordinal in the block: `start + count - 1`. Infallible:
157    /// [`try_new`](Self::try_new) validated `count >= 1` (so `count - 1` does not
158    /// underflow) and that `start + (count - 1)` fits in `u64`, so a constructed
159    /// `SeqGrant` witnesses this cannot panic. The `start + (count - 1)`
160    /// association matches that check exactly — computing `(start + count) - 1`
161    /// would overflow the intermediate at the `last() == u64::MAX` boundary even
162    /// though the result fits.
163    pub fn last(&self) -> u64 {
164        self.start + (u64::from(self.count) - 1)
165    }
166}
167
168#[derive(Debug)]
169enum SeqState {
170    NotLeader,
171    Leader { epoch: Epoch },
172}
173
174/// Leadership/epoch gate plus request validation for the dense path. Holds NO
175/// per-key counter state — counters live in the durable layer and `start` is
176/// assigned there. This type only decides "may this request proceed, and is it
177/// well-formed?".
178pub struct SeqAllocator {
179    state: SeqState,
180}
181
182impl SeqAllocator {
183    pub fn new() -> Self {
184        SeqAllocator {
185            state: SeqState::NotLeader,
186        }
187    }
188
189    /// Transition to leader state for `epoch`. The caller must already hold
190    /// consensus leadership for `epoch` before calling this.
191    pub fn become_leader(&mut self, epoch: Epoch) {
192        self.state = SeqState::Leader { epoch };
193    }
194
195    pub fn step_down(&mut self) {
196        self.state = SeqState::NotLeader;
197    }
198
199    pub fn is_leader(&self) -> bool {
200        matches!(self.state, SeqState::Leader { .. })
201    }
202
203    pub fn epoch(&self) -> Option<Epoch> {
204        match self.state {
205            SeqState::Leader { epoch } => Some(epoch),
206            SeqState::NotLeader => None,
207        }
208    }
209
210    /// Validate a request without touching durable state. Leadership is checked
211    /// first; then count bounds (zero, then oversized), then key validity.
212    /// Returns the validated [`SeqKey`].
213    ///
214    /// `max_count` is the caller's per-call ceiling — the server's configured
215    /// [`ServerBuilder::max_seq_count`], which defaults to
216    /// [`DEFAULT_MAX_SEQ_COUNT`]. Injecting it (rather than reading the constant
217    /// here) keeps the cap a server-side policy the operator can tune, while the
218    /// `count >= 1` floor stays a hard invariant enforced unconditionally.
219    ///
220    /// [`ServerBuilder::max_seq_count`]: https://docs.rs/tsoracle-server
221    pub fn validate_request(
222        &self,
223        key: &str,
224        count: u32,
225        max_count: u32,
226    ) -> Result<SeqKey, CoreError> {
227        if !self.is_leader() {
228            return Err(CoreError::NotLeader);
229        }
230        if count == 0 {
231            return Err(CoreError::SeqCountZero);
232        }
233        if count > max_count {
234            return Err(CoreError::SeqCountTooLarge {
235                count,
236                max: max_count,
237            });
238        }
239        SeqKey::try_new(key)
240    }
241}
242
243impl Default for SeqAllocator {
244    fn default() -> Self {
245        Self::new()
246    }
247}
248
249#[cfg(test)]
250mod seqallocator_tests {
251    use super::*;
252
253    #[test]
254    fn new_is_not_leader() {
255        let a = SeqAllocator::new();
256        assert!(!a.is_leader());
257        assert_eq!(a.epoch(), None);
258    }
259
260    #[test]
261    fn become_leader_then_step_down() {
262        let mut a = SeqAllocator::new();
263        a.become_leader(Epoch(3));
264        assert!(a.is_leader());
265        assert_eq!(a.epoch(), Some(Epoch(3)));
266        a.step_down();
267        assert!(!a.is_leader());
268        assert_eq!(a.epoch(), None);
269    }
270
271    #[test]
272    fn validate_request_off_leader_is_not_leader() {
273        let a = SeqAllocator::new();
274        assert_eq!(
275            a.validate_request("orders", 1, DEFAULT_MAX_SEQ_COUNT),
276            Err(CoreError::NotLeader)
277        );
278    }
279
280    #[test]
281    fn validate_request_rejects_zero_count() {
282        let mut a = SeqAllocator::new();
283        a.become_leader(Epoch(1));
284        assert_eq!(
285            a.validate_request("orders", 0, DEFAULT_MAX_SEQ_COUNT),
286            Err(CoreError::SeqCountZero)
287        );
288    }
289
290    #[test]
291    fn validate_request_rejects_oversized_count() {
292        let mut a = SeqAllocator::new();
293        a.become_leader(Epoch(1));
294        assert_eq!(
295            a.validate_request("orders", DEFAULT_MAX_SEQ_COUNT + 1, DEFAULT_MAX_SEQ_COUNT),
296            Err(CoreError::SeqCountTooLarge {
297                count: DEFAULT_MAX_SEQ_COUNT + 1,
298                max: DEFAULT_MAX_SEQ_COUNT
299            })
300        );
301    }
302
303    #[test]
304    fn validate_request_uses_caller_provided_max() {
305        // The ceiling is the `max_count` argument, not the default constant: a
306        // smaller cap rejects below the default, and a larger cap accepts above
307        // it — proving the cap is injected policy, not a hard-coded limit.
308        let mut a = SeqAllocator::new();
309        a.become_leader(Epoch(1));
310        assert_eq!(
311            a.validate_request("orders", 11, 10),
312            Err(CoreError::SeqCountTooLarge { count: 11, max: 10 }),
313            "count above a small configured cap must be rejected",
314        );
315        assert!(
316            a.validate_request("orders", 10, 10).is_ok(),
317            "count at the configured cap must be accepted",
318        );
319        assert!(
320            a.validate_request(
321                "orders",
322                DEFAULT_MAX_SEQ_COUNT + 1,
323                DEFAULT_MAX_SEQ_COUNT * 2
324            )
325            .is_ok(),
326            "a larger configured cap must accept counts above the default",
327        );
328    }
329
330    #[test]
331    fn validate_request_rejects_bad_key() {
332        let mut a = SeqAllocator::new();
333        a.become_leader(Epoch(1));
334        assert_eq!(
335            a.validate_request("", 1, DEFAULT_MAX_SEQ_COUNT),
336            Err(CoreError::SeqKeyEmpty)
337        );
338    }
339
340    #[test]
341    fn validate_request_ok_returns_key() {
342        let mut a = SeqAllocator::new();
343        a.become_leader(Epoch(1));
344        let k = a
345            .validate_request("orders", 10, DEFAULT_MAX_SEQ_COUNT)
346            .unwrap();
347        assert_eq!(k.as_str(), "orders");
348    }
349
350    #[test]
351    fn validate_request_accepts_max_count_exactly() {
352        let mut a = SeqAllocator::new();
353        a.become_leader(Epoch(1));
354        assert!(
355            a.validate_request("orders", DEFAULT_MAX_SEQ_COUNT, DEFAULT_MAX_SEQ_COUNT)
356                .is_ok()
357        );
358    }
359}
360
361#[cfg(test)]
362mod seqgrant_tests {
363    use super::*;
364
365    #[test]
366    fn exposes_fields_and_last() {
367        let key = SeqKey::try_new("users").unwrap();
368        let g = SeqGrant::try_new(key.clone(), 100, 5, Epoch(7)).unwrap();
369        assert_eq!(g.key().as_str(), "users");
370        assert_eq!(g.start(), 100);
371        assert_eq!(g.count(), 5);
372        assert_eq!(g.epoch(), Epoch(7));
373        // [100, 105): last issued ordinal is 104.
374        assert_eq!(g.last(), 104);
375    }
376
377    #[test]
378    fn last_equals_start_when_count_is_one() {
379        let g1 = SeqGrant::try_new(SeqKey::try_new("x").unwrap(), 42, 1, Epoch(1)).unwrap();
380        assert_eq!(g1.last(), 42);
381    }
382
383    #[test]
384    fn try_new_rejects_zero_count() {
385        let key = SeqKey::try_new("x").unwrap();
386        assert_eq!(
387            SeqGrant::try_new(key, 5, 0, Epoch(1)),
388            Err(CoreError::SeqCountZero)
389        );
390    }
391
392    #[test]
393    fn try_new_rejects_block_overflow() {
394        // start + count - 1 overflows u64, which would make `last()` wrap.
395        let key = SeqKey::try_new("x").unwrap();
396        assert_eq!(
397            SeqGrant::try_new(key, u64::MAX, 2, Epoch(1)),
398            Err(CoreError::SeqBlockOverflow {
399                start: u64::MAX,
400                count: 2
401            })
402        );
403    }
404
405    #[test]
406    fn try_new_accepts_max_boundary() {
407        // start + count - 1 == u64::MAX exactly is the largest valid block.
408        let key = SeqKey::try_new("x").unwrap();
409        let g = SeqGrant::try_new(key, u64::MAX - 4, 5, Epoch(1)).unwrap();
410        assert_eq!(g.last(), u64::MAX);
411    }
412}
413
414#[cfg(test)]
415mod seqkey_tests {
416    use super::*;
417
418    #[test]
419    fn accepts_normal_key() {
420        let k = SeqKey::try_new("orders").unwrap();
421        assert_eq!(k.as_str(), "orders");
422    }
423
424    #[test]
425    fn rejects_empty() {
426        assert_eq!(SeqKey::try_new(""), Err(CoreError::SeqKeyEmpty));
427    }
428
429    #[test]
430    fn accepts_max_length() {
431        let s = "a".repeat(MAX_SEQ_KEY_LEN);
432        assert!(SeqKey::try_new(&s).is_ok());
433    }
434
435    #[test]
436    fn rejects_one_past_max_length() {
437        let s = "a".repeat(MAX_SEQ_KEY_LEN + 1);
438        assert_eq!(
439            SeqKey::try_new(&s),
440            Err(CoreError::SeqKeyTooLong {
441                len: MAX_SEQ_KEY_LEN + 1,
442                max: MAX_SEQ_KEY_LEN
443            })
444        );
445    }
446
447    #[test]
448    fn length_is_measured_in_utf8_bytes_not_chars() {
449        // 'é' is 2 bytes; 64 of them = 128 bytes = exactly the cap.
450        let ok = "é".repeat(MAX_SEQ_KEY_LEN / 2);
451        assert!(SeqKey::try_new(&ok).is_ok());
452        let too_long = "é".repeat(MAX_SEQ_KEY_LEN / 2 + 1);
453        assert!(matches!(
454            SeqKey::try_new(&too_long),
455            Err(CoreError::SeqKeyTooLong { .. })
456        ));
457    }
458
459    // ---- SeqKey: serde re-validates on deserialize (try_new is the single
460    // validation site, including the postcard decode path the openraft log and
461    // dense snapshot travel through). A derived `Deserialize` would build the
462    // inner `String` directly and bypass `try_new`, so these pin that decode
463    // routes through the invariant.
464
465    #[cfg(feature = "serde")]
466    #[test]
467    fn serde_deserialize_rejects_empty_key() {
468        // `postcard::to_stdvec(&String::new())` is a structurally valid postcard
469        // String (a single zero length-prefix byte). Decoding it as a `SeqKey`
470        // must re-run `try_new` and reject the empty key, not return `SeqKey("")`.
471        let bytes = postcard::to_stdvec(&String::new()).expect("encode empty string");
472        let decoded = postcard::from_bytes::<SeqKey>(&bytes);
473        assert!(
474            decoded.is_err(),
475            "empty-key postcard payload must fail to decode, got {decoded:?}",
476        );
477    }
478
479    #[cfg(feature = "serde")]
480    #[test]
481    fn serde_deserialize_rejects_oversized_key() {
482        let oversized = "a".repeat(MAX_SEQ_KEY_LEN + 1);
483        let bytes = postcard::to_stdvec(&oversized).expect("encode oversized string");
484        let decoded = postcard::from_bytes::<SeqKey>(&bytes);
485        assert!(
486            decoded.is_err(),
487            "oversized-key postcard payload must fail to decode, got {decoded:?}",
488        );
489    }
490
491    #[cfg(feature = "serde")]
492    #[test]
493    fn serde_round_trip_preserves_valid_key() {
494        let key = SeqKey::try_new("orders").unwrap();
495        let bytes = postcard::to_stdvec(&key).expect("encode key");
496        // Serialize is newtype-transparent: the bytes are exactly a postcard
497        // `String`, so the on-disk/wire format is unchanged by routing decode
498        // through `try_new`.
499        assert_eq!(bytes, postcard::to_stdvec(&"orders".to_string()).unwrap());
500        let back: SeqKey = postcard::from_bytes(&bytes).expect("decode key");
501        assert_eq!(back, key);
502        assert_eq!(back.as_str(), "orders");
503    }
504
505    #[cfg(feature = "serde")]
506    #[test]
507    fn serde_deserialize_accepts_max_length_key() {
508        let max = "a".repeat(MAX_SEQ_KEY_LEN);
509        let bytes = postcard::to_stdvec(&max).expect("encode max-length string");
510        let back: SeqKey = postcard::from_bytes(&bytes).expect("max-length key must decode");
511        assert_eq!(back.as_str(), max);
512    }
513}