1use std::cmp::Ordering;
8use std::error::Error;
9use std::fmt;
10use std::hash::{Hash, Hasher};
11use std::time::Duration;
12
13use crate::{CommitSequence, DatabaseId, FrontierPosition};
14
15const FRESHNESS_BYTES_V1: u8 = 0x01;
17const FRESHNESS_BYTES_V2: u8 = 0x02;
19
20const POSITION_BEFORE_FIRST: u8 = 0x00;
22const POSITION_APPLIED_THROUGH: u8 = 0x01;
24
25#[derive(Clone)]
30pub struct CommitToken {
31 bytes: Vec<u8>,
32 database_id: Option<DatabaseId>,
33 history_incarnation: u64,
34 commit_sequence: CommitSequence,
35}
36
37impl CommitToken {
38 #[must_use]
40 pub fn new(history_incarnation: u64, commit_sequence: CommitSequence) -> Self {
41 let mut bytes = Vec::with_capacity(1 + 8 + 1 + 8);
42 bytes.push(FRESHNESS_BYTES_V1);
43 bytes.extend_from_slice(&history_incarnation.to_be_bytes());
44 bytes.push(POSITION_APPLIED_THROUGH);
45 bytes.extend_from_slice(&commit_sequence.to_be_bytes());
46 Self {
47 bytes,
48 database_id: None,
49 history_incarnation,
50 commit_sequence,
51 }
52 }
53
54 #[must_use]
56 pub fn new_scoped(
57 database_id: DatabaseId,
58 history_incarnation: u64,
59 commit_sequence: CommitSequence,
60 ) -> Self {
61 let mut token = Self::new(history_incarnation, commit_sequence);
62 token.bytes = scoped_bytes(database_id, &token.bytes);
63 token.database_id = Some(database_id);
64 token
65 }
66
67 pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, FreshnessTokenError> {
69 let (database_id, history_incarnation, position) = decode_frontier_payload(&bytes)?;
70 let FrontierPosition::AppliedThrough(commit_sequence) = position else {
71 return Err(FreshnessTokenError::InvalidShape);
72 };
73 Ok(Self {
74 bytes,
75 database_id,
76 history_incarnation,
77 commit_sequence,
78 })
79 }
80
81 #[must_use]
83 pub const fn database_id(&self) -> Option<DatabaseId> {
84 self.database_id
85 }
86
87 #[must_use]
89 pub fn frontier(&self) -> ProjectionFrontier {
90 ProjectionFrontier {
91 bytes: self.bytes.clone(),
92 database_id: self.database_id,
93 history_incarnation: self.history_incarnation,
94 position: FrontierPosition::AppliedThrough(self.commit_sequence),
95 }
96 }
97
98 #[must_use]
100 pub const fn history_incarnation(&self) -> u64 {
101 self.history_incarnation
102 }
103
104 #[must_use]
106 pub const fn commit_sequence(&self) -> CommitSequence {
107 self.commit_sequence
108 }
109
110 #[must_use]
112 pub fn as_bytes(&self) -> &[u8] {
113 &self.bytes
114 }
115
116 #[must_use]
118 pub fn into_bytes(self) -> Vec<u8> {
119 self.bytes
120 }
121}
122
123#[derive(Clone)]
129pub struct ProjectionFrontier {
130 bytes: Vec<u8>,
131 database_id: Option<DatabaseId>,
132 history_incarnation: u64,
133 position: FrontierPosition,
134}
135
136impl ProjectionFrontier {
137 #[must_use]
139 pub fn new(history_incarnation: u64, position: FrontierPosition) -> Self {
140 let mut bytes = Vec::with_capacity(1 + 8 + 1 + 8);
141 bytes.push(FRESHNESS_BYTES_V1);
142 bytes.extend_from_slice(&history_incarnation.to_be_bytes());
143 match position {
144 FrontierPosition::BeforeFirst => {
145 bytes.push(POSITION_BEFORE_FIRST);
146 }
147 FrontierPosition::AppliedThrough(sequence) => {
148 bytes.push(POSITION_APPLIED_THROUGH);
149 bytes.extend_from_slice(&sequence.to_be_bytes());
150 }
151 }
152 Self {
153 bytes,
154 database_id: None,
155 history_incarnation,
156 position,
157 }
158 }
159
160 #[must_use]
162 pub fn new_scoped(
163 database_id: DatabaseId,
164 history_incarnation: u64,
165 position: FrontierPosition,
166 ) -> Self {
167 let mut frontier = Self::new(history_incarnation, position);
168 frontier.bytes = scoped_bytes(database_id, &frontier.bytes);
169 frontier.database_id = Some(database_id);
170 frontier
171 }
172
173 pub fn from_bytes(bytes: Vec<u8>) -> Result<Self, FreshnessTokenError> {
175 let (database_id, history_incarnation, position) = decode_frontier_payload(&bytes)?;
176 Ok(Self {
177 bytes,
178 database_id,
179 history_incarnation,
180 position,
181 })
182 }
183
184 #[must_use]
186 pub const fn database_id(&self) -> Option<DatabaseId> {
187 self.database_id
188 }
189
190 #[must_use]
192 pub fn commit_token(&self) -> Option<CommitToken> {
193 let FrontierPosition::AppliedThrough(commit_sequence) = self.position else {
194 return None;
195 };
196 Some(CommitToken {
197 bytes: self.bytes.clone(),
198 database_id: self.database_id,
199 history_incarnation: self.history_incarnation,
200 commit_sequence,
201 })
202 }
203
204 #[must_use]
206 pub const fn history_incarnation(&self) -> u64 {
207 self.history_incarnation
208 }
209
210 #[must_use]
212 pub const fn position(&self) -> FrontierPosition {
213 self.position
214 }
215
216 #[must_use]
218 pub fn as_bytes(&self) -> &[u8] {
219 &self.bytes
220 }
221
222 #[must_use]
224 pub fn into_bytes(self) -> Vec<u8> {
225 self.bytes
226 }
227
228 #[must_use]
232 pub fn satisfies(&self, required: &CommitToken) -> bool {
233 if self.database_id != required.database_id
234 || self.history_incarnation != required.history_incarnation
235 {
236 return false;
237 }
238 match self.position {
239 FrontierPosition::BeforeFirst => false,
240 FrontierPosition::AppliedThrough(current) => {
241 current.get() >= required.commit_sequence.get()
242 }
243 }
244 }
245
246 #[must_use]
249 pub fn lag_sequences(&self, head: &Self) -> Option<u64> {
250 if self.database_id != head.database_id
251 || self.history_incarnation != head.history_incarnation
252 {
253 return None;
254 }
255 match (self.position, head.position) {
256 (FrontierPosition::BeforeFirst, FrontierPosition::AppliedThrough(head_seq)) => {
257 Some(head_seq.get())
258 }
259 (
260 FrontierPosition::AppliedThrough(current_seq),
261 FrontierPosition::AppliedThrough(head_seq),
262 ) => head_seq.get().checked_sub(current_seq.get()),
263 _ => None,
264 }
265 }
266}
267
268#[derive(Clone, Debug, Eq, PartialEq)]
273pub enum FreshnessPolicy {
274 Causal {
276 token: CommitToken,
278 max_wait: Duration,
280 },
281 Bounded {
283 max_lag_sequences: u64,
285 },
286 Available,
288}
289
290#[derive(Clone, Copy, Debug, Eq, PartialEq)]
292pub enum FreshnessTokenError {
293 TruncatedOrTrailing,
295 InvalidShape,
297 ZeroCommitSequence,
299}
300
301impl fmt::Display for FreshnessTokenError {
302 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
303 formatter.write_str(match self {
304 Self::TruncatedOrTrailing => "freshness token bytes are truncated or trailing",
305 Self::InvalidShape => "freshness token bytes have an invalid shape",
306 Self::ZeroCommitSequence => "commit sequence must be nonzero",
307 })
308 }
309}
310
311impl Error for FreshnessTokenError {}
312
313fn scoped_bytes(database_id: DatabaseId, legacy: &[u8]) -> Vec<u8> {
314 let mut bytes = Vec::with_capacity(legacy.len() + 16);
315 bytes.push(FRESHNESS_BYTES_V2);
316 bytes.extend_from_slice(database_id.as_bytes());
317 bytes.extend_from_slice(&legacy[1..]);
318 bytes
319}
320
321fn decode_frontier_payload(
322 bytes: &[u8],
323) -> Result<(Option<DatabaseId>, u64, FrontierPosition), FreshnessTokenError> {
324 let (database_id, payload) = match bytes.first() {
325 Some(&FRESHNESS_BYTES_V1) => (None, &bytes[1..]),
326 Some(&FRESHNESS_BYTES_V2) => {
327 if bytes.len() < 17 {
328 return Err(FreshnessTokenError::TruncatedOrTrailing);
329 }
330 let database = DatabaseId::from_bytes(bytes[1..17].try_into().expect("length checked"))
331 .map_err(|_| FreshnessTokenError::InvalidShape)?;
332 (Some(database), &bytes[17..])
333 }
334 None => return Err(FreshnessTokenError::TruncatedOrTrailing),
335 _ => return Err(FreshnessTokenError::InvalidShape),
336 };
337 if payload.len() < 8 + 1 {
338 return Err(FreshnessTokenError::TruncatedOrTrailing);
339 }
340 let history_incarnation = u64::from_be_bytes(payload[..8].try_into().expect("length checked"));
341 match payload[8] {
342 POSITION_BEFORE_FIRST => {
343 if payload.len() != 9 {
344 return Err(FreshnessTokenError::TruncatedOrTrailing);
345 }
346 Ok((
347 database_id,
348 history_incarnation,
349 FrontierPosition::BeforeFirst,
350 ))
351 }
352 POSITION_APPLIED_THROUGH => {
353 if payload.len() != 17 {
354 return Err(FreshnessTokenError::TruncatedOrTrailing);
355 }
356 let sequence = u64::from_be_bytes(payload[9..17].try_into().expect("length checked"));
357 let commit_sequence =
358 CommitSequence::new(sequence).ok_or(FreshnessTokenError::ZeroCommitSequence)?;
359 Ok((
360 database_id,
361 history_incarnation,
362 FrontierPosition::AppliedThrough(commit_sequence),
363 ))
364 }
365 _ => Err(FreshnessTokenError::InvalidShape),
366 }
367}
368
369macro_rules! freshness_byte_traits {
370 ($type:ty) => {
371 impl PartialEq for $type {
372 fn eq(&self, other: &Self) -> bool {
373 self.bytes == other.bytes
374 }
375 }
376
377 impl Eq for $type {}
378
379 impl PartialOrd for $type {
380 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
381 Some(self.cmp(other))
382 }
383 }
384
385 impl Ord for $type {
386 fn cmp(&self, other: &Self) -> Ordering {
387 self.bytes.cmp(&other.bytes)
388 }
389 }
390
391 impl Hash for $type {
392 fn hash<H: Hasher>(&self, state: &mut H) {
393 self.bytes.hash(state);
394 }
395 }
396 };
397}
398
399freshness_byte_traits!(CommitToken);
400freshness_byte_traits!(ProjectionFrontier);
401
402impl fmt::Debug for CommitToken {
403 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
404 formatter
405 .debug_struct("CommitToken")
406 .field("bytes", &"[REDACTED]")
407 .field("length", &self.bytes.len())
408 .finish()
409 }
410}
411
412impl fmt::Debug for ProjectionFrontier {
413 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
414 formatter
415 .debug_struct("ProjectionFrontier")
416 .field("bytes", &"[REDACTED]")
417 .field("length", &self.bytes.len())
418 .finish()
419 }
420}
421
422impl TryFrom<Vec<u8>> for CommitToken {
423 type Error = FreshnessTokenError;
424
425 fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
426 Self::from_bytes(value)
427 }
428}
429
430impl TryFrom<Vec<u8>> for ProjectionFrontier {
431 type Error = FreshnessTokenError;
432
433 fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
434 Self::from_bytes(value)
435 }
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441
442 fn seq(value: u64) -> CommitSequence {
443 CommitSequence::new(value).expect("nonzero")
444 }
445
446 fn database(seed: u8) -> DatabaseId {
447 DatabaseId::from_unix_milliseconds_and_random(42, [seed; 10]).unwrap()
448 }
449
450 #[test]
452 fn scoped_freshness_v2_preserves_golden_v1_and_v2_bytes() {
453 let legacy = CommitToken::new(3, seq(42));
454 let token = CommitToken::new_scoped(database(1), 3, seq(42));
455 let before = ProjectionFrontier::new_scoped(database(1), 3, FrontierPosition::BeforeFirst);
456 let fixtures = include_str!("../../../fixtures/freshness/opaque-v1-v2.txt")
457 .lines()
458 .collect::<Vec<_>>();
459 let hex = |bytes: &[u8]| {
460 bytes
461 .iter()
462 .map(|byte| format!("{byte:02x}"))
463 .collect::<String>()
464 };
465 assert_eq!(hex(legacy.as_bytes()), fixtures[0]);
466 assert_eq!(hex(token.as_bytes()), fixtures[1]);
467 assert_eq!(hex(before.as_bytes()), fixtures[2]);
468 for expected in [legacy, token] {
469 let decoded = CommitToken::from_bytes(expected.as_bytes().to_vec()).unwrap();
470 assert_eq!(decoded, expected);
471 let frontier = decoded.frontier();
472 assert_eq!(frontier.commit_token().unwrap(), decoded);
473 assert_eq!(
474 ProjectionFrontier::from_bytes(frontier.as_bytes().to_vec()).unwrap(),
475 frontier
476 );
477 assert!(frontier.satisfies(&decoded));
478 }
479 assert_eq!(
480 ProjectionFrontier::from_bytes(before.as_bytes().to_vec()).unwrap(),
481 before
482 );
483 assert!(before.commit_token().is_none());
484 assert!(CommitToken::from_bytes(before.into_bytes()).is_err());
485 }
486
487 #[test]
489 fn scoped_freshness_refuses_foreign_database_incarnation_and_unbound_v1() {
490 let token = CommitToken::new_scoped(database(1), 3, seq(42));
491 let frontier = token.frontier();
492 for other in [
493 CommitToken::new_scoped(database(2), 3, seq(42)),
494 CommitToken::new_scoped(database(1), 4, seq(42)),
495 CommitToken::new(3, seq(42)),
496 ] {
497 assert_ne!(token, other);
498 assert!(!frontier.satisfies(&other));
499 assert!(!other.frontier().satisfies(&token));
500 assert_eq!(frontier.lag_sequences(&other.frontier()), None);
501 }
502 assert!(frontier.satisfies(&CommitToken::new_scoped(database(1), 3, seq(41))));
503 assert!(!frontier.satisfies(&CommitToken::new_scoped(database(1), 3, seq(43))));
504 assert_eq!(
505 format!("{token:?}"),
506 "CommitToken { bytes: \"[REDACTED]\", length: 34 }"
507 );
508 }
509
510 #[test]
512 fn scoped_freshness_decoder_refuses_every_truncation_and_noncanonical_shape() {
513 let bytes = CommitToken::new_scoped(database(1), 3, seq(42)).into_bytes();
514 for length in 0..bytes.len() {
515 assert!(CommitToken::from_bytes(bytes[..length].to_vec()).is_err());
516 assert!(ProjectionFrontier::from_bytes(bytes[..length].to_vec()).is_err());
517 }
518 let mut trailing = bytes.clone();
519 trailing.push(0);
520 assert!(CommitToken::from_bytes(trailing.clone()).is_err());
521 assert!(ProjectionFrontier::from_bytes(trailing).is_err());
522 for (offset, value) in [(0, 3), (7, 0), (9, 0), (25, 2), (33, 0)] {
523 let mut corrupt = bytes.clone();
524 corrupt[offset] = value;
525 assert!(CommitToken::from_bytes(corrupt.clone()).is_err());
526 assert!(ProjectionFrontier::from_bytes(corrupt).is_err());
527 }
528 }
529
530 #[test]
531 fn commit_token_round_trips_opaque_bytes() {
532 let token = CommitToken::new(3, seq(42));
533 let decoded = CommitToken::from_bytes(token.as_bytes().to_vec()).expect("decode");
534 assert_eq!(decoded.history_incarnation(), 3);
535 assert_eq!(decoded.commit_sequence(), seq(42));
536 assert_eq!(decoded, token);
537 let debug = format!("{token:?}");
538 assert!(debug.contains("REDACTED"));
539 assert!(!debug.contains("42"));
540 }
541
542 #[test]
543 fn projection_frontier_round_trips_before_first_and_applied() {
544 let before = ProjectionFrontier::new(1, FrontierPosition::BeforeFirst);
545 let decoded = ProjectionFrontier::from_bytes(before.as_bytes().to_vec()).expect("decode");
546 assert_eq!(decoded.position(), FrontierPosition::BeforeFirst);
547 assert_eq!(decoded.history_incarnation(), 1);
548
549 let applied = ProjectionFrontier::new(2, FrontierPosition::AppliedThrough(seq(9)));
550 let decoded = ProjectionFrontier::from_bytes(applied.as_bytes().to_vec()).expect("decode");
551 assert_eq!(decoded.position(), FrontierPosition::AppliedThrough(seq(9)));
552 assert_eq!(decoded, applied);
553 }
554
555 #[test]
556 fn frontier_satisfies_requires_matching_incarnation() {
557 let frontier = ProjectionFrontier::new(1, FrontierPosition::AppliedThrough(seq(10)));
558 let same = CommitToken::new(1, seq(10));
559 let earlier = CommitToken::new(1, seq(7));
560 let later = CommitToken::new(1, seq(11));
561 let restored = CommitToken::new(2, seq(5));
562
563 assert!(frontier.satisfies(&same));
564 assert!(frontier.satisfies(&earlier));
565 assert!(!frontier.satisfies(&later));
566 assert!(
568 !frontier.satisfies(&restored),
569 "stale pre-restore token must not satisfy a new incarnation frontier"
570 );
571 }
572
573 #[test]
574 fn lag_sequences_is_none_across_incarnations() {
575 let current = ProjectionFrontier::new(1, FrontierPosition::AppliedThrough(seq(3)));
576 let head = ProjectionFrontier::new(1, FrontierPosition::AppliedThrough(seq(10)));
577 assert_eq!(current.lag_sequences(&head), Some(7));
578 let other = ProjectionFrontier::new(2, FrontierPosition::AppliedThrough(seq(10)));
579 assert_eq!(current.lag_sequences(&other), None);
580 }
581}