Skip to main content

reddb_wire/replication/
bookmark.rs

1//! Causal bookmark token contract.
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub struct CausalBookmark {
5    term: u64,
6    commit_lsn: u64,
7}
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum BookmarkDecodeError {
11    InvalidPrefix,
12    InvalidLength,
13    InvalidHex,
14}
15
16impl std::fmt::Display for BookmarkDecodeError {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        match self {
19            Self::InvalidPrefix => write!(f, "invalid causal bookmark prefix"),
20            Self::InvalidLength => write!(f, "invalid causal bookmark length"),
21            Self::InvalidHex => write!(f, "invalid causal bookmark hex payload"),
22        }
23    }
24}
25
26impl std::error::Error for BookmarkDecodeError {}
27
28impl CausalBookmark {
29    pub fn new(term: u64, commit_lsn: u64) -> Self {
30        Self { term, commit_lsn }
31    }
32
33    pub fn term(self) -> u64 {
34        self.term
35    }
36
37    pub fn commit_lsn(self) -> u64 {
38        self.commit_lsn
39    }
40
41    pub fn encode(self) -> String {
42        format!("rbm1.{:016x}{:016x}", self.term, self.commit_lsn)
43    }
44
45    pub fn decode(token: &str) -> Result<Self, BookmarkDecodeError> {
46        let Some(payload) = token.strip_prefix("rbm1.") else {
47            return Err(BookmarkDecodeError::InvalidPrefix);
48        };
49        if payload.len() != 32 {
50            return Err(BookmarkDecodeError::InvalidLength);
51        }
52        let term =
53            u64::from_str_radix(&payload[..16], 16).map_err(|_| BookmarkDecodeError::InvalidHex)?;
54        let commit_lsn =
55            u64::from_str_radix(&payload[16..], 16).map_err(|_| BookmarkDecodeError::InvalidHex)?;
56        Ok(Self { term, commit_lsn })
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn causal_bookmark_round_trips_stable_wire_token() {
66        let bookmark = CausalBookmark::new(0x12, 0x345);
67        let token = bookmark.encode();
68        assert_eq!(token, "rbm1.00000000000000120000000000000345");
69        assert_eq!(CausalBookmark::decode(&token).unwrap(), bookmark);
70    }
71
72    #[test]
73    fn causal_bookmark_rejects_bad_tokens() {
74        assert_eq!(
75            CausalBookmark::decode("bad.00000000000000010000000000000002").unwrap_err(),
76            BookmarkDecodeError::InvalidPrefix
77        );
78        assert_eq!(
79            CausalBookmark::decode("rbm1.1").unwrap_err(),
80            BookmarkDecodeError::InvalidLength
81        );
82        assert_eq!(
83            CausalBookmark::decode("rbm1.000000000000000x0000000000000002").unwrap_err(),
84            BookmarkDecodeError::InvalidHex
85        );
86    }
87}