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}