Skip to main content

slipstream/
protocol.rs

1//! Protocol decision kernels — the load-bearing guards of the snapshot
2//! export/import protocol, extracted as pure functions so the PRODUCTION
3//! code and the exhaustive model checker (`tests/model.rs`) execute the
4//! **same logic**, not two hand-synchronized copies of it.
5//!
6//! Every function here is a guard whose correctness the machine-checked
7//! theorems depend on. The call sites:
8//!
9//! | kernel                     | production                          | model              |
10//! |----------------------------|-------------------------------------|--------------------|
11//! | [`pointer_publish_allowed`]| `transport::swap_pointer`           | `Act::Publish`     |
12//! | [`payload_prunable`]       | `transport::ObjectStoreTransport::prune` | `Act::Prune`  |
13//! | [`resume_window_ok`]       | `nats` resume paths (`check_resume_window`) | `Act::Resume` |
14//!
15//! Because the model transitions call these very functions, a change to any
16//! guard is re-verified against the full bounded state space on the next
17//! `cargo test --test model` — the guards cannot drift from the proof. The
18//! mutation tests in `tests/model.rs` additionally prove each guard is
19//! load-bearing: substituting a broken variant makes the checker produce a
20//! counterexample.
21//!
22//! Kernels operate on plain `u64` ranks (a [`WatchCursor`](crate::WatchCursor)'s
23//! revision, with revisionless cursors ranked 0 by the callers) so they stay
24//! free of I/O types and usable from the checker's `u8`-bounded state space.
25
26/// What the publisher observed at the pointer key before deciding.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum PointerState {
29    /// No pointer object exists — the slot is open (create-only publish).
30    Absent,
31    /// A pointer object exists. `rank` is its cursor's rank, or `None` when
32    /// the object is unparseable — a corrupt pointer MUST be replaceable, or
33    /// one bad write wedges publishing forever (the same rule as the export
34    /// lease's corrupt-steal).
35    Present {
36        /// Rank of the existing pointer's cursor; `None` if unparseable.
37        rank: Option<u64>,
38    },
39}
40
41/// THE monotonic pointer guard: may `candidate_rank` be published over the
42/// observed `current` pointer?
43///
44/// `true` for an open slot, a corrupt pointer, or a candidate at or above
45/// the existing cursor; `false` exactly when the existing pointer is
46/// parseable and STRICTLY newer — the refusal that makes a slow exporter's
47/// stale publish a no-op instead of a regression.
48///
49/// Soundness of deciding on a read (before the conditional put): every
50/// writer uses this guard with a compare-and-swap, so the pointer's rank is
51/// monotone non-decreasing — once "strictly newer" is observed, it can never
52/// become false, so refusal needs no CAS. Machine-checked as `published
53/// cursor never regresses` in `tests/model.rs`.
54pub fn pointer_publish_allowed(current: &PointerState, candidate_rank: u64) -> bool {
55    match current {
56        PointerState::Absent => true,
57        PointerState::Present { rank: None } => true,
58        PointerState::Present {
59            rank: Some(existing),
60        } => candidate_rank >= *existing,
61    }
62}
63
64/// THE prune guard: may this payload object be deleted, given the current
65/// pointer's rank?
66///
67/// A payload is prunable only when ALL hold:
68/// - it is not the pointer's own target;
69/// - its rank is parseable AND **strictly below** the pointer's (an
70///   unparseable rank is never deleted — unknown objects are not ours);
71/// - its age has cleared the grace period (`aged_out`; the model passes
72///   `true`, checking the harshest zero-grace timing).
73///
74/// Strictly-below is what makes a dangling pointer impossible regardless of
75/// timing: [`pointer_publish_allowed`] refuses any candidate below the
76/// pointer, and the pointer is monotone — so anything this guard deletes
77/// (rank < pointer-at-prune ≤ pointer-at-any-later-swap) can never be
78/// successfully published afterward. The model checker FOUND the dangling
79/// counterexample under the earlier age-only rule (a same-cursor payload
80/// collected mid-publish, then published by the `>=` swap guard); this rule
81/// is the structural fix, machine-checked as `pointer target always
82/// fetchable` under zero-grace pruning.
83pub fn payload_prunable(
84    payload_rank: Option<u64>,
85    pointer_rank: u64,
86    is_pointer_target: bool,
87    aged_out: bool,
88) -> bool {
89    !is_pointer_target && aged_out && payload_rank.is_some_and(|rank| rank < pointer_rank)
90}
91
92/// THE cursor-expiry guard: is resuming from `revision` sound, given the
93/// stream's first retained sequence?
94///
95/// A resume reads `revision + 1` onward; it is sound iff nothing at or below
96/// `revision + 1`'s predecessor gap has been head-evicted — i.e.
97/// `first_sequence <= revision + 1`. Interior (per-subject) eviction inside
98/// the gap is safe for a last-write-wins fold (an overwrite-evicted revision
99/// implies a later revision of the same subject exists and will be
100/// delivered); lost DELETES come from head eviction, which is exactly what
101/// advances `first_sequence`.
102///
103/// This check must be performed by US: NATS does not error on a below-head
104/// start sequence — it silently clamps to the first retained message
105/// (pinned live by `tests/resync.rs::nats_silently_clamps_resume_below_first_seq`),
106/// which would skip the gap's evicted delete markers with no fallback and no
107/// resync. Machine-checked as `bootstrap never silently diverges` in
108/// `tests/model.rs` (where the model's retention floor is
109/// `first_sequence - 1`).
110pub fn resume_window_ok(revision: u64, first_sequence: u64) -> bool {
111    first_sequence <= revision.saturating_add(1)
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn publish_guard_boundaries() {
120        let absent = PointerState::Absent;
121        let corrupt = PointerState::Present { rank: None };
122        let at = |r| PointerState::Present { rank: Some(r) };
123        assert!(pointer_publish_allowed(&absent, 0));
124        assert!(pointer_publish_allowed(&corrupt, 0));
125        assert!(pointer_publish_allowed(&at(5), 5), "equal republishes");
126        assert!(pointer_publish_allowed(&at(5), 6));
127        assert!(!pointer_publish_allowed(&at(5), 4), "stale is refused");
128    }
129
130    #[test]
131    fn prune_guard_boundaries() {
132        // Strictly below, aged, not the target: prunable.
133        assert!(payload_prunable(Some(4), 5, false, true));
134        // Equal rank is NOT prunable — it is still publishable (>= guard).
135        assert!(!payload_prunable(Some(5), 5, false, true));
136        assert!(!payload_prunable(Some(6), 5, false, true));
137        // The pointer's own target and unparseable ranks are never prunable.
138        assert!(!payload_prunable(Some(4), 5, true, true));
139        assert!(!payload_prunable(None, 5, false, true));
140        // Grace window holds everything.
141        assert!(!payload_prunable(Some(4), 5, false, false));
142    }
143
144    #[test]
145    fn resume_guard_boundaries() {
146        assert!(resume_window_ok(3, 4), "first retained == next read: sound");
147        assert!(resume_window_ok(3, 1), "history intact");
148        assert!(!resume_window_ok(3, 5), "gap head-evicted: expired");
149        assert!(resume_window_ok(u64::MAX, u64::MAX), "saturating boundary");
150    }
151}