Skip to main content

riffdb_types/
dual_frontier.rs

1//! The dual durable frontier ADR-0100 addresses changelog frames by.
2//!
3//! ADR-0101 publishes two coordinated sequences at one durability fence: the
4//! application commit sequence and the administration sequence. ADR-0100 ยง1
5//! makes the pair the address of a changelog frame โ€” a frame names its
6//! predecessor pair and its covered pair, "encoding an unchanged frontier
7//! explicitly".
8//!
9//! This module owns only the semantic identity of that pair. It deliberately
10//! does not touch [`crate::CommitToken`] or [`crate::ProjectionFrontier`],
11//! which remain application-sequence-only freshness vocabulary (ADR-0086).
12
13use std::cmp::Ordering;
14use std::fmt;
15
16use crate::{AdministrationSequence, CommitSequence};
17
18/// Encoded byte length of a canonical [`DualFrontier`].
19pub const DUAL_FRONTIER_BYTES: usize = 18;
20
21/// Explicit tag for an absent (never-advanced) frontier component.
22const COMPONENT_ABSENT: u8 = 0x00;
23/// Explicit tag for a present nonzero frontier component.
24const COMPONENT_PRESENT: u8 = 0x01;
25
26/// One published durable frontier: an application and an administration position.
27///
28/// `None` in either component means that space has never advanced. The
29/// canonical encoding tags presence explicitly so an unchanged frontier is
30/// never confused with a zero sequence.
31///
32/// Comparison is the componentwise (product) order and is deliberately
33/// **partial**: a pair whose application component rises while its
34/// administration component falls is *incomparable*, not "greater". Every
35/// caller that needs "does this advance?" must use [`DualFrontier::advances_from`],
36/// which answers `false` for an incomparable pair. This is the same fail-closed
37/// discipline [`crate::ProjectionFrontier::satisfies`] applies across
38/// incarnations: an unproven relation is never treated as satisfied.
39#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
40pub struct DualFrontier {
41    application: Option<CommitSequence>,
42    administration: Option<AdministrationSequence>,
43}
44
45impl DualFrontier {
46    /// The frontier of a database on which nothing has ever been published.
47    pub const INITIAL: Self = Self {
48        application: None,
49        administration: None,
50    };
51
52    /// Builds a frontier pair from its two independently optional components.
53    #[must_use]
54    pub const fn new(
55        application: Option<CommitSequence>,
56        administration: Option<AdministrationSequence>,
57    ) -> Self {
58        Self {
59            application,
60            administration,
61        }
62    }
63
64    /// Returns the published application commit-sequence component.
65    #[must_use]
66    pub const fn application(self) -> Option<CommitSequence> {
67        self.application
68    }
69
70    /// Returns the published administration-sequence component.
71    #[must_use]
72    pub const fn administration(self) -> Option<AdministrationSequence> {
73        self.administration
74    }
75
76    /// Returns true when nothing has ever been published.
77    #[must_use]
78    pub const fn is_initial(self) -> bool {
79        self.application.is_none() && self.administration.is_none()
80    }
81
82    /// Returns true when `self` is a strict successor of `predecessor`.
83    ///
84    /// Both components must be greater than or equal to the predecessor's and
85    /// at least one must be strictly greater. An incomparable pair โ€” one
86    /// component up, the other down โ€” answers `false`.
87    #[must_use]
88    pub fn advances_from(self, predecessor: Self) -> bool {
89        self.partial_cmp(&predecessor) == Some(Ordering::Greater)
90    }
91
92    /// Returns the canonical [`DUAL_FRONTIER_BYTES`]-byte encoding.
93    ///
94    /// Layout: `[app presence][app u64 BE][admin presence][admin u64 BE]`.
95    /// An absent component encodes its presence tag plus eight zero bytes, so
96    /// "unchanged" is explicit rather than inferred.
97    #[must_use]
98    pub const fn to_canonical_bytes(self) -> [u8; DUAL_FRONTIER_BYTES] {
99        let mut encoded = [0_u8; DUAL_FRONTIER_BYTES];
100        if let Some(application) = self.application {
101            encoded[0] = COMPONENT_PRESENT;
102            let value = application.to_be_bytes();
103            let mut index = 0;
104            while index < 8 {
105                encoded[1 + index] = value[index];
106                index += 1;
107            }
108        } else {
109            encoded[0] = COMPONENT_ABSENT;
110        }
111        if let Some(administration) = self.administration {
112            encoded[9] = COMPONENT_PRESENT;
113            let value = administration.to_be_bytes();
114            let mut index = 0;
115            while index < 8 {
116                encoded[10 + index] = value[index];
117                index += 1;
118            }
119        } else {
120            encoded[9] = COMPONENT_ABSENT;
121        }
122        encoded
123    }
124
125    /// Decodes a canonical encoding, failing closed on any noncanonical shape.
126    pub fn from_canonical_bytes(
127        bytes: [u8; DUAL_FRONTIER_BYTES],
128    ) -> Result<Self, DualFrontierError> {
129        let application = decode_component(bytes[0], &bytes[1..9])?
130            .map(|value| CommitSequence::new(value).ok_or(DualFrontierError::ZeroSequence))
131            .transpose()?;
132        let administration = decode_component(bytes[9], &bytes[10..18])?
133            .map(|value| AdministrationSequence::new(value).ok_or(DualFrontierError::ZeroSequence))
134            .transpose()?;
135        Ok(Self::new(application, administration))
136    }
137}
138
139fn decode_component(presence: u8, value: &[u8]) -> Result<Option<u64>, DualFrontierError> {
140    let raw = u64::from_be_bytes(
141        value
142            .try_into()
143            .map_err(|_| DualFrontierError::InvalidShape)?,
144    );
145    match presence {
146        COMPONENT_ABSENT if raw == 0 => Ok(None),
147        COMPONENT_ABSENT => Err(DualFrontierError::NonCanonicalAbsence),
148        COMPONENT_PRESENT => Ok(Some(raw)),
149        _ => Err(DualFrontierError::InvalidShape),
150    }
151}
152
153impl PartialOrd for DualFrontier {
154    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
155        let application = component_cmp(
156            self.application.map(CommitSequence::get),
157            other.application.map(CommitSequence::get),
158        );
159        let administration = component_cmp(
160            self.administration.map(AdministrationSequence::get),
161            other.administration.map(AdministrationSequence::get),
162        );
163        match (application, administration) {
164            (Ordering::Equal, value) | (value, Ordering::Equal) => Some(value),
165            (Ordering::Less, Ordering::Less) => Some(Ordering::Less),
166            (Ordering::Greater, Ordering::Greater) => Some(Ordering::Greater),
167            _ => None,
168        }
169    }
170}
171
172fn component_cmp(left: Option<u64>, right: Option<u64>) -> Ordering {
173    left.unwrap_or(0).cmp(&right.unwrap_or(0))
174}
175
176impl fmt::Display for DualFrontier {
177    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
178        match (self.application, self.administration) {
179            (Some(application), Some(administration)) => {
180                write!(formatter, "(app {application}, admin {administration})")
181            }
182            (Some(application), None) => write!(formatter, "(app {application}, admin none)"),
183            (None, Some(administration)) => write!(formatter, "(app none, admin {administration})"),
184            (None, None) => formatter.write_str("(app none, admin none)"),
185        }
186    }
187}
188
189/// A safe failure to decode a canonical [`DualFrontier`].
190#[derive(Clone, Copy, Debug, Eq, PartialEq)]
191pub enum DualFrontierError {
192    /// A presence tag is not one of the two closed values.
193    InvalidShape,
194    /// A component is tagged absent but carries nonzero bytes.
195    NonCanonicalAbsence,
196    /// A component is tagged present but encodes sequence zero.
197    ZeroSequence,
198}
199
200impl fmt::Display for DualFrontierError {
201    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
202        formatter.write_str(match self {
203            Self::InvalidShape => "dual frontier bytes have an invalid shape",
204            Self::NonCanonicalAbsence => {
205                "an absent dual frontier component is not canonically zero"
206            }
207            Self::ZeroSequence => "a present dual frontier component must be nonzero",
208        })
209    }
210}
211
212impl std::error::Error for DualFrontierError {}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    fn application(value: u64) -> Option<CommitSequence> {
219        CommitSequence::new(value)
220    }
221
222    fn administration(value: u64) -> Option<AdministrationSequence> {
223        AdministrationSequence::new(value)
224    }
225
226    fn pair(app: u64, admin: u64) -> DualFrontier {
227        DualFrontier::new(application(app), administration(admin))
228    }
229
230    #[test]
231    fn initial_is_both_components_absent() {
232        assert!(DualFrontier::INITIAL.is_initial());
233        assert_eq!(DualFrontier::INITIAL.application(), None);
234        assert_eq!(DualFrontier::INITIAL.administration(), None);
235        assert!(!pair(1, 0).is_initial());
236    }
237
238    #[test]
239    fn canonical_bytes_round_trip_over_every_presence_combination() {
240        for frontier in [
241            DualFrontier::INITIAL,
242            pair(1, 0),
243            pair(0, 1),
244            pair(7, 9),
245            pair(u64::MAX, u64::MAX),
246        ] {
247            let encoded = frontier.to_canonical_bytes();
248            assert_eq!(encoded.len(), DUAL_FRONTIER_BYTES);
249            assert_eq!(
250                DualFrontier::from_canonical_bytes(encoded).expect("canonical round trip"),
251                frontier
252            );
253        }
254    }
255
256    #[test]
257    fn absence_is_encoded_explicitly_and_never_confused_with_zero() {
258        let encoded = DualFrontier::INITIAL.to_canonical_bytes();
259        assert_eq!(encoded, [0_u8; DUAL_FRONTIER_BYTES]);
260        let present_zero = {
261            let mut bytes = encoded;
262            bytes[0] = COMPONENT_PRESENT;
263            bytes
264        };
265        assert_eq!(
266            DualFrontier::from_canonical_bytes(present_zero),
267            Err(DualFrontierError::ZeroSequence)
268        );
269        let absent_nonzero = {
270            let mut bytes = encoded;
271            bytes[8] = 1;
272            bytes
273        };
274        assert_eq!(
275            DualFrontier::from_canonical_bytes(absent_nonzero),
276            Err(DualFrontierError::NonCanonicalAbsence)
277        );
278        let unknown_tag = {
279            let mut bytes = encoded;
280            bytes[9] = 0x02;
281            bytes
282        };
283        assert_eq!(
284            DualFrontier::from_canonical_bytes(unknown_tag),
285            Err(DualFrontierError::InvalidShape)
286        );
287    }
288
289    #[test]
290    fn a_command_frame_advances_only_the_application_component() {
291        let predecessor = pair(3, 4);
292        let covered = pair(5, 4);
293        assert!(covered.advances_from(predecessor));
294        assert!(!predecessor.advances_from(covered));
295        assert_eq!(covered.partial_cmp(&predecessor), Some(Ordering::Greater));
296    }
297
298    #[test]
299    fn a_service_audit_frame_advances_only_the_administration_component() {
300        let predecessor = pair(5, 4);
301        let covered = pair(5, 6);
302        assert!(covered.advances_from(predecessor));
303        assert!(!predecessor.advances_from(covered));
304    }
305
306    #[test]
307    fn an_equal_pair_never_advances() {
308        assert!(!pair(5, 4).advances_from(pair(5, 4)));
309        assert!(!DualFrontier::INITIAL.advances_from(DualFrontier::INITIAL));
310        assert_eq!(pair(5, 4).partial_cmp(&pair(5, 4)), Some(Ordering::Equal));
311    }
312
313    #[test]
314    fn the_first_frame_advances_from_the_initial_frontier() {
315        assert!(pair(1, 0).advances_from(DualFrontier::INITIAL));
316        assert!(pair(0, 1).advances_from(DualFrontier::INITIAL));
317        assert!(pair(1, 1).advances_from(DualFrontier::INITIAL));
318    }
319
320    #[test]
321    fn an_incomparable_pair_is_never_an_advance_in_either_direction() {
322        let left = pair(9, 2);
323        let right = pair(3, 7);
324        assert_eq!(left.partial_cmp(&right), None);
325        assert_eq!(right.partial_cmp(&left), None);
326        assert!(!left.advances_from(right));
327        assert!(!right.advances_from(left));
328    }
329
330    #[test]
331    fn a_regressed_component_is_never_an_advance() {
332        assert!(!pair(4, 4).advances_from(pair(5, 4)));
333        assert!(!pair(5, 3).advances_from(pair(5, 4)));
334        assert!(!DualFrontier::INITIAL.advances_from(pair(1, 0)));
335    }
336
337    #[test]
338    fn display_names_both_components_without_leaking_anything_else() {
339        assert_eq!(pair(5, 4).to_string(), "(app 5, admin 4)");
340        assert_eq!(pair(5, 0).to_string(), "(app 5, admin none)");
341        assert_eq!(pair(0, 4).to_string(), "(app none, admin 4)");
342        assert_eq!(DualFrontier::INITIAL.to_string(), "(app none, admin none)");
343    }
344}