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