Skip to main content

didbot_schema/
tid.rs

1//! Timestamp identifiers: the sortable keys this server mints records under.
2//!
3//! `didbot-pds`'s record store mints TIDs, and this module reads one back. A
4//! minter with no matching parser is a format nothing can check, and the
5//! first thing a wrong bit in an rkey costs is the ordering the whole store is
6//! built on.
7//!
8//! A TID is a 64-bit integer rendered as thirteen characters of a base32
9//! alphabet ordered so that ASCII order matches numeric order. The layout is
10//! one clear top bit, 53 bits of microseconds since the Unix epoch, and a
11//! 10-bit clock identifier.
12//!
13//! [`Minter`] is the other half: the one place a TID is made. Record keys
14//! come from it and so do the revisions a commit is stamped with, and they
15//! come from the same one per store so that neither can repeat the other.
16
17use std::sync::Mutex;
18use std::time::{SystemTime, UNIX_EPOCH};
19
20/// The alphabet a TID is written in: base32 with the digits and letters
21/// reordered so that string order matches numeric order.
22pub const BASE32_SORTABLE: &[u8; 32] = b"234567abcdefghijklmnopqrstuvwxyz";
23
24/// How many characters a TID always is. Not a maximum: a TID is never padded
25/// and never abbreviated.
26pub const TID_LENGTH: usize = 13;
27
28/// Why a string is not a TID.
29#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
30pub enum TidError {
31    /// Not exactly [`TID_LENGTH`] characters.
32    #[error("tid is {0} characters, must be exactly {TID_LENGTH}")]
33    WrongLength(usize),
34    /// A character outside [`BASE32_SORTABLE`]. Case matters: `3JZFCIJPJ2Z2A`
35    /// is not the same string as `3jzfcijpj2z2a` and is not a TID.
36    #[error("invalid character {0:?} in tid: not sortable base32")]
37    InvalidCharacter(char),
38    /// The top bit of the 64-bit value is set.
39    ///
40    /// The leading character therefore has to fall in the first half of the
41    /// alphabet, which is what rules out `zzzzzzzzzzzzz`. The bit is reserved,
42    /// and a value that uses it is a different format wearing this one's
43    /// shape.
44    #[error("tid has its high bit set: leading character {0:?} is out of range")]
45    HighBitSet(char),
46}
47
48/// A parsed TID: the timestamp it carries and the clock that minted it.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
50pub struct Tid {
51    micros: u64,
52    clock_id: u16,
53}
54
55impl Tid {
56    /// Microseconds since the Unix epoch, as the key encodes them.
57    ///
58    /// This is what the minter's clock said, not a verified time. A TID says
59    /// when its writer believed it was writing.
60    pub fn micros(self) -> u64 {
61        self.micros
62    }
63
64    /// The 10-bit clock identifier that distinguishes concurrent minters.
65    pub fn clock_id(self) -> u16 {
66        self.clock_id
67    }
68}
69
70/// Checks a string against the TID rules.
71///
72/// ```
73/// use didbot_schema::tid::validate;
74/// assert!(validate("3jzfcijpj2z2a").is_ok());
75/// assert!(validate("3JZFCIJPJ2Z2A").is_err()); // case-sensitive
76/// assert!(validate("zzzzzzzzzzzzz").is_err()); // high bit set
77/// ```
78pub fn validate(tid: &str) -> Result<(), TidError> {
79    parse(tid).map(|_| ())
80}
81
82/// Decodes a TID into the timestamp and clock identifier it carries.
83pub fn parse(tid: &str) -> Result<Tid, TidError> {
84    // Length is counted in characters rather than bytes so that a string of
85    // thirteen multi-byte characters is rejected for its characters and not
86    // for a byte count that would read as a puzzling off-by-26.
87    let length = tid.chars().count();
88    if length != TID_LENGTH {
89        return Err(TidError::WrongLength(length));
90    }
91    let mut value: u64 = 0;
92    for (index, ch) in tid.chars().enumerate() {
93        let digit = BASE32_SORTABLE
94            .iter()
95            .position(|candidate| u32::from(*candidate) == u32::from(ch))
96            .ok_or(TidError::InvalidCharacter(ch))?;
97        // The leading character contributes bits 63..=59, and bit 63 is
98        // reserved. Only the first sixteen letters of the alphabet leave it
99        // clear.
100        if index == 0 && digit >= 16 {
101            return Err(TidError::HighBitSet(ch));
102        }
103        value = (value << 5) | digit as u64;
104    }
105    Ok(Tid {
106        micros: (value >> 10) & ((1 << 53) - 1),
107        clock_id: (value & 0x3ff) as u16,
108    })
109}
110
111/// Encodes a 64-bit value as 13 characters of [`BASE32_SORTABLE`].
112///
113/// The alphabet's ordering is the whole point: it lets a store keep records in
114/// a plain ordered map and get newest-first listing from the key alone.
115fn encode_key(value: u64) -> String {
116    let mut out = [b'2'; 13];
117    let mut remaining = value;
118    for slot in out.iter_mut().rev() {
119        *slot = BASE32_SORTABLE[(remaining & 0x1f) as usize];
120        remaining >>= 5;
121    }
122    // Every byte came out of a 32-entry ASCII table.
123    String::from_utf8_lossy(&out).into_owned()
124}
125
126/// Reads back a key [`encode_key`] produced, if the string is one.
127///
128/// Used only on replay, to move a fresh minter past the keys a previous run
129/// of this process already handed out. Anything that is not thirteen
130/// characters of the sortable alphabet is not a key this server minted, and
131/// answers `None` rather than a number derived from part of it.
132fn decode_key(key: &str) -> Option<u64> {
133    if key.len() != 13 {
134        return None;
135    }
136    let mut value = 0u64;
137    for byte in key.bytes() {
138        let digit = BASE32_SORTABLE
139            .iter()
140            .position(|candidate| *candidate == byte)?;
141        value = (value << 5) | digit as u64;
142    }
143    Some(value)
144}
145
146/// Mints TIDs that sort in the order they were minted.
147///
148/// A TID as atproto specifies it: microseconds since the Unix epoch in the
149/// low 53 bits above a 10-bit clock identifier, with the top bit clear,
150/// rendered in [`BASE32_SORTABLE`]. Two properties matter. It is timestamped,
151/// so a TID says when it was minted without a second field beside it; and it
152/// is sortable as a string, so newest-first is a reverse iteration of an
153/// ordered map rather than a sort.
154///
155/// One minter serves a store's record keys and its commit revisions together.
156/// They are separate sequences to a reader and the same sequence here, which
157/// costs nothing — a revision is not required to be dense — and buys the one
158/// property both need: that nothing this store hands out ever repeats or
159/// sorts backwards, whatever the clock does.
160#[derive(Debug)]
161pub struct Minter {
162    clock_id: u64,
163    last: Mutex<u64>,
164}
165
166impl Minter {
167    /// A minter with a fixed clock identifier.
168    ///
169    /// The clock id is derived from the process start rather than randomly,
170    /// because this store is per-process and never merges with another: its
171    /// only job is to keep two processes writing the same logical repository
172    /// from colliding, which cannot happen to an in-memory store anyway.
173    #[must_use]
174    pub fn new() -> Self {
175        Self {
176            clock_id: micros_now() & 0x3ff,
177            last: Mutex::new(0),
178        }
179    }
180
181    /// The next TID, strictly greater than every one this minter has returned.
182    ///
183    /// The clock is only read, never trusted for ordering. Two writes inside
184    /// one microsecond, or a clock that steps backwards, would otherwise
185    /// produce a key that sorts before an earlier record and silently reorder
186    /// a listing.
187    pub fn next(&self) -> String {
188        let mut last = self
189            .last
190            .lock()
191            .unwrap_or_else(|poisoned| poisoned.into_inner());
192        let micros = micros_now().max(*last + 1);
193        *last = micros;
194        encode_key((micros << 10) | self.clock_id)
195    }
196
197    /// Notes a TID somebody else minted, so this minter never repeats it.
198    ///
199    /// A restart replays keys a previous process wrote, and the clock is the
200    /// only thing that would otherwise keep the new process past them. That
201    /// is enough while the clock moves forward and is exactly the assumption
202    /// [`Minter::next`] already refuses to make, so replay states it here
203    /// instead: a key that sorted after everything when it was written must
204    /// still sort after everything when the log is read back.
205    pub fn observe(&self, minted: &str) {
206        let Some(value) = decode_key(minted) else {
207            return;
208        };
209        let mut last = self
210            .last
211            .lock()
212            .unwrap_or_else(|poisoned| poisoned.into_inner());
213        *last = (*last).max(value >> 10);
214    }
215}
216
217impl Default for Minter {
218    fn default() -> Self {
219        Self::new()
220    }
221}
222
223/// Microseconds since the Unix epoch, clamped into a TID's 53 bits.
224///
225/// A clock before the epoch reads as the epoch. It is not worth an error
226/// path: the resulting key is still unique and still sorts, because
227/// [`Minter::next`] enforces both regardless of what the clock says.
228fn micros_now() -> u64 {
229    let micros = SystemTime::now()
230        .duration_since(UNIX_EPOCH)
231        .map(|since| since.as_micros())
232        .unwrap_or_default();
233    u64::try_from(micros).unwrap_or(u64::MAX) & ((1 << 53) - 1)
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn keys_mint_in_ascending_order_even_within_one_microsecond() {
242        let minter = Minter::new();
243        let keys: Vec<String> = (0..1000).map(|_| minter.next()).collect();
244        let mut sorted = keys.clone();
245        sorted.sort();
246        assert_eq!(keys, sorted, "record keys must sort in minting order");
247        sorted.dedup();
248        assert_eq!(sorted.len(), keys.len(), "record keys must be unique");
249    }
250
251    #[test]
252    fn keys_are_thirteen_characters_of_the_sortable_alphabet() {
253        let key = Minter::new().next();
254        assert_eq!(key.len(), 13, "a tid is 13 characters: {key}");
255        assert!(
256            key.bytes().all(|b| BASE32_SORTABLE.contains(&b)),
257            "{key} is not base32-sortable"
258        );
259    }
260
261    #[test]
262    fn a_key_decodes_back_to_what_it_encoded() {
263        for value in [0u64, 1, 31, 32, 1 << 40, (1 << 63) - 1] {
264            assert_eq!(decode_key(&encode_key(value)), Some(value));
265        }
266    }
267
268    #[test]
269    fn anything_that_is_not_a_minted_key_decodes_to_nothing() {
270        assert_eq!(decode_key("self"), None);
271        assert_eq!(decode_key(""), None);
272        // Thirteen characters, but `0`, `1`, `8` and `9` are not in the
273        // sortable alphabet — it starts at `2` so that the digits and the
274        // letters sort in one run.
275        assert_eq!(decode_key("1111111111111"), None);
276        assert_eq!(decode_key("9999999999999"), None);
277    }
278
279    /// A restart replays keys a previous process minted. A fresh minter must
280    /// come out above them even if the clock says otherwise, or a listing
281    /// silently reorders across the restart.
282    #[test]
283    fn a_minter_never_repeats_a_key_it_was_shown() {
284        let minter = Minter::new();
285        let ahead = encode_key(((micros_now() + 60_000_000) << 10) | 7);
286        minter.observe(&ahead);
287        let next = minter.next();
288        assert!(next > ahead, "{next} should sort after {ahead}");
289    }
290
291    #[test]
292    fn encoding_preserves_numeric_order() {
293        assert!(encode_key(1) < encode_key(2));
294        assert!(encode_key(31) < encode_key(32));
295        assert!(encode_key(1 << 40) < encode_key(1 << 41));
296    }
297}