Skip to main content

tsoracle_driver_openraft/
log_entry.rs

1//
2//  ░▀█▀░█▀▀░█▀█░█▀▄░█▀█░█▀▀░█░░░█▀▀
3//  ░░█░░▀▀█░█░█░█▀▄░█▀█░█░░░█░░░█▀▀
4//  ░░▀░░▀▀▀░▀▀▀░▀░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀
5//
6//  tsoracle — Distributed Timestamp Oracle
7//  https://www.tsoracle.rs
8//
9//  Copyright (c) 2026 Prisma Risk
10//
11//  Licensed under the Apache License, Version 2.0 (the "License");
12//  you may not use this file except in compliance with the License.
13//  You may obtain a copy of the License at
14//
15//      https://www.apache.org/licenses/LICENSE-2.0
16//
17//  Unless required by applicable law or agreed to in writing, software
18//  distributed under the License is distributed on an "AS IS" BASIS,
19//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20//  See the License for the specific language governing permissions and
21//  limitations under the License.
22//
23
24// #[PerformanceCriticalPath]
25//! Log entries replicated by the openraft cluster.
26//!
27//! The driver replicates a single command: advance the high-water mark to at
28//! least [`AdvancePayload::at_least`]. The state machine treats this as
29//! `current = max(current, at_least)`, which makes the operation idempotent
30//! under retries and monotone under reordering — matching the
31//! [`tsoracle_consensus::ConsensusDriver`] "advance to at least" contract. The
32//! payload is the cross-backend [`tsoracle_consensus::AdvancePayload`], shared
33//! with the paxos driver so the "advance" command carries one name and one
34//! field across backends.
35
36use std::collections::BTreeSet;
37use std::fmt;
38
39use serde::{Deserialize, Serialize};
40use tsoracle_consensus::AdvancePayload;
41
42/// Payload of [`HighWaterCommand::SetFormatVersion`]: the committed activation
43/// barrier that flips the cluster's active write version.
44///
45/// `gated_members` is the exact member set (voters ∪ learners) the leader's
46/// activation gate observed as capable of reading `target` at proposal time.
47/// It travels inside the entry because the state-machine apply cannot perform
48/// a live capability query; apply re-validates that the membership committed
49/// as of this entry's own log position is a subset of `gated_members` before
50/// taking effect. `NodeId` is `u64` (the type config's `Node` id type), so the
51/// set is `BTreeSet<u64>` — a deterministic, ordered, postcard-stable layout.
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53pub struct SetFormatVersionPayload {
54    /// The active write version to flip to on a successful (non-no-op) apply.
55    pub target: u8,
56    /// Voters ∪ learners the leader gated as `target`-readable at proposal time.
57    pub gated_members: BTreeSet<u64>,
58}
59
60/// One `(key, count)` entry of an [`HighWaterCommand::AdvanceDenseBatch`]. A
61/// named struct (not a tuple) so the postcard layout is stable and pinnable and
62/// the embedded `SeqKey` revalidates on decode exactly like single-key
63/// `AdvanceDense`.
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65pub struct DenseAdvance {
66    pub key: tsoracle_core::SeqKey,
67    pub count: u32,
68}
69
70/// Commands the state machine knows how to apply.
71///
72/// `Display` is implemented because openraft's `AppData` blanket requires it
73/// (used in the `Entry`'s human-readable summary).
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75pub enum HighWaterCommand {
76    /// Advance the high-water mark to at least `at_least`. Idempotent.
77    Advance(AdvancePayload),
78    /// Activate a new active write version, gated on the carried member set.
79    /// Applies as a no-op if the membership at this entry's log position is
80    /// not a subset of `gated_members`.
81    SetFormatVersion(SetFormatVersionPayload),
82    /// Advance the dense counter for `key` by `count`, lazily creating the key
83    /// at 0. The pre-advance value is the issued block's start; it is returned
84    /// to the proposer via [`ApplyOutcome`](crate::ApplyOutcome) (the apply path computes it in
85    /// committed log order). Only ever appended under write version
86    /// >= DENSE_WRITE_VERSION (gated by activation).
87    AdvanceDense {
88        key: tsoracle_core::SeqKey,
89        count: u32,
90    },
91    /// Atomically advance several distinct dense counters in one entry. Applied
92    /// all-or-nothing: a cardinality or overflow rejection moves no counter (see
93    /// the state-machine apply). Each entry's pre-advance value is the issued
94    /// block's start, returned in request order via
95    /// [`ApplyOutcome::DenseBatchAdvanced`](crate::ApplyOutcome). Only ever
96    /// appended under write version >= BATCH_WRITE_VERSION (gated by activation).
97    AdvanceDenseBatch { entries: Vec<DenseAdvance> },
98}
99
100impl fmt::Display for HighWaterCommand {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        match self {
103            HighWaterCommand::Advance(AdvancePayload { at_least }) => {
104                write!(f, "Advance {{ at_least: {at_least} }}")
105            }
106            HighWaterCommand::SetFormatVersion(SetFormatVersionPayload {
107                target,
108                gated_members,
109            }) => {
110                let rendered: Vec<String> = gated_members.iter().map(|id| id.to_string()).collect();
111                write!(
112                    f,
113                    "SetFormatVersion {{ target: {target}, gated_members: [{}] }}",
114                    rendered.join(", ")
115                )
116            }
117            HighWaterCommand::AdvanceDense { key, count } => {
118                write!(
119                    f,
120                    "AdvanceDense {{ key: {}, count: {count} }}",
121                    key.as_str()
122                )
123            }
124            HighWaterCommand::AdvanceDenseBatch { entries } => {
125                let rendered: Vec<String> = entries
126                    .iter()
127                    .map(|e| format!("{} x{}", e.key.as_str(), e.count))
128                    .collect();
129                write!(
130                    f,
131                    "AdvanceDenseBatch {{ entries: [{}] }}",
132                    rendered.join(", ")
133                )
134            }
135        }
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn display_renders_advance() {
145        let cmd = HighWaterCommand::Advance(AdvancePayload { at_least: 42 });
146        assert_eq!(format!("{cmd}"), "Advance { at_least: 42 }");
147    }
148
149    #[test]
150    fn display_renders_zero_at_least() {
151        let cmd = HighWaterCommand::Advance(AdvancePayload { at_least: 0 });
152        assert_eq!(format!("{cmd}"), "Advance { at_least: 0 }");
153    }
154
155    #[test]
156    fn postcard_round_trip_advance() {
157        let cmd = HighWaterCommand::Advance(AdvancePayload {
158            at_least: 1_234_567_890,
159        });
160        let bytes = postcard::to_stdvec(&cmd).expect("serialize");
161        let back: HighWaterCommand = postcard::from_bytes(&bytes).expect("deserialize");
162        assert_eq!(back, cmd);
163    }
164
165    #[test]
166    fn postcard_round_trip_zero() {
167        let cmd = HighWaterCommand::Advance(AdvancePayload { at_least: 0 });
168        let bytes = postcard::to_stdvec(&cmd).expect("serialize");
169        let back: HighWaterCommand = postcard::from_bytes(&bytes).expect("deserialize");
170        assert_eq!(back, cmd);
171    }
172
173    #[test]
174    fn postcard_round_trip_max() {
175        let cmd = HighWaterCommand::Advance(AdvancePayload { at_least: u64::MAX });
176        let bytes = postcard::to_stdvec(&cmd).expect("serialize");
177        let back: HighWaterCommand = postcard::from_bytes(&bytes).expect("deserialize");
178        assert_eq!(back, cmd);
179    }
180
181    #[test]
182    fn display_renders_set_format_version() {
183        let cmd = HighWaterCommand::SetFormatVersion(SetFormatVersionPayload {
184            target: 4,
185            gated_members: BTreeSet::from([1u64, 2u64, 3u64]),
186        });
187        assert_eq!(
188            format!("{cmd}"),
189            "SetFormatVersion { target: 4, gated_members: [1, 2, 3] }"
190        );
191    }
192
193    #[test]
194    fn postcard_round_trip_set_format_version() {
195        let cmd = HighWaterCommand::SetFormatVersion(SetFormatVersionPayload {
196            target: 7,
197            gated_members: BTreeSet::from([10u64, 20u64]),
198        });
199        let bytes = postcard::to_stdvec(&cmd).expect("serialize");
200        let back: HighWaterCommand = postcard::from_bytes(&bytes).expect("deserialize");
201        assert_eq!(back, cmd);
202    }
203
204    #[test]
205    fn postcard_round_trip_set_format_version_empty_gate() {
206        // The gated set may legitimately be empty for a degenerate re-issue;
207        // it must still round-trip so the apply-time subset check sees
208        // exactly what the proposer recorded.
209        let cmd = HighWaterCommand::SetFormatVersion(SetFormatVersionPayload {
210            target: 4,
211            gated_members: BTreeSet::new(),
212        });
213        let bytes = postcard::to_stdvec(&cmd).expect("serialize");
214        let back: HighWaterCommand = postcard::from_bytes(&bytes).expect("deserialize");
215        assert_eq!(back, cmd);
216    }
217
218    #[test]
219    fn postcard_round_trip_advance_dense() {
220        // A valid `AdvanceDense` round-trips unchanged: routing the embedded
221        // `SeqKey` decode through `try_new` must not perturb the honest path.
222        let cmd = HighWaterCommand::AdvanceDense {
223            key: tsoracle_core::SeqKey::try_new("orders").unwrap(),
224            count: 7,
225        };
226        let bytes = postcard::to_stdvec(&cmd).expect("serialize");
227        let back: HighWaterCommand = postcard::from_bytes(&bytes).expect("deserialize");
228        assert_eq!(back, cmd);
229    }
230
231    #[test]
232    fn advance_dense_postcard_layout_is_pinned() {
233        // Pin the byte layout the hand-crafted invalid-key payloads below rely
234        // on: `AdvanceDense` is the 3rd `HighWaterCommand` variant (index 2),
235        // followed by the newtype-transparent `SeqKey` (a postcard `String`:
236        // varint length prefix + UTF-8 bytes) then the `count` varint. A
237        // 1-byte key "a" with count 1 is therefore [2, 1, b'a', 1].
238        let cmd = HighWaterCommand::AdvanceDense {
239            key: tsoracle_core::SeqKey::try_new("a").unwrap(),
240            count: 1,
241        };
242        let bytes = postcard::to_stdvec(&cmd).expect("serialize");
243        assert_eq!(bytes, vec![2u8, 1, b'a', 1]);
244    }
245
246    #[test]
247    fn decode_rejects_advance_dense_empty_key() {
248        // Variant 2 (AdvanceDense), empty-string key (length prefix 0), count 1.
249        // The bytes are structurally valid postcard, but the embedded `SeqKey`
250        // violates the non-empty invariant, so the decode must fail loud rather
251        // than land `SeqKey("")` in a replicated command.
252        let bytes = vec![2u8, 0, 1];
253        let decoded = postcard::from_bytes::<HighWaterCommand>(&bytes);
254        assert!(
255            decoded.is_err(),
256            "AdvanceDense with empty key must fail to decode, got {decoded:?}",
257        );
258    }
259
260    #[test]
261    fn decode_rejects_advance_dense_oversized_key() {
262        // Variant 2, a 129-byte key (one past MAX_SEQ_KEY_LEN), count 1. 129 as
263        // a postcard varint is [0x81, 0x01].
264        let mut bytes = vec![2u8, 0x81, 0x01];
265        bytes.extend(std::iter::repeat_n(b'a', 129));
266        bytes.push(1);
267        let decoded = postcard::from_bytes::<HighWaterCommand>(&bytes);
268        assert!(
269            decoded.is_err(),
270            "AdvanceDense with oversized key must fail to decode, got {decoded:?}",
271        );
272    }
273
274    #[test]
275    fn display_renders_advance_dense_batch() {
276        let cmd = HighWaterCommand::AdvanceDenseBatch {
277            entries: vec![
278                DenseAdvance {
279                    key: tsoracle_core::SeqKey::try_new("orders").unwrap(),
280                    count: 3,
281                },
282                DenseAdvance {
283                    key: tsoracle_core::SeqKey::try_new("users").unwrap(),
284                    count: 5,
285                },
286            ],
287        };
288        assert_eq!(
289            format!("{cmd}"),
290            "AdvanceDenseBatch { entries: [orders x3, users x5] }"
291        );
292    }
293
294    #[test]
295    fn postcard_round_trip_advance_dense_batch() {
296        let cmd = HighWaterCommand::AdvanceDenseBatch {
297            entries: vec![DenseAdvance {
298                key: tsoracle_core::SeqKey::try_new("orders").unwrap(),
299                count: 7,
300            }],
301        };
302        let bytes = postcard::to_stdvec(&cmd).expect("serialize");
303        let back: HighWaterCommand = postcard::from_bytes(&bytes).expect("deserialize");
304        assert_eq!(back, cmd);
305    }
306
307    #[test]
308    fn advance_dense_batch_postcard_layout_is_pinned() {
309        // AdvanceDenseBatch is the 4th HighWaterCommand variant (index 3),
310        // followed by a postcard seq (varint len prefix) of DenseAdvance, each
311        // = SeqKey (String: varint len + bytes) then count varint. One entry
312        // {"a", 1} is therefore [3, 1, 1, b'a', 1]:
313        //   3 = variant index, 1 = vec len, 1 = key len, b'a' = key, 1 = count.
314        let cmd = HighWaterCommand::AdvanceDenseBatch {
315            entries: vec![DenseAdvance {
316                key: tsoracle_core::SeqKey::try_new("a").unwrap(),
317                count: 1,
318            }],
319        };
320        let bytes = postcard::to_stdvec(&cmd).expect("serialize");
321        assert_eq!(bytes, vec![3u8, 1, 1, b'a', 1]);
322    }
323
324    #[test]
325    fn decode_rejects_advance_dense_batch_empty_key() {
326        // Variant 3, vec len 1, key len 0 (empty), count 1. Structurally valid
327        // postcard, but the embedded SeqKey violates the non-empty invariant,
328        // so decode must fail loud rather than land SeqKey("") in a command.
329        let bytes = vec![3u8, 1, 0, 1];
330        let decoded = postcard::from_bytes::<HighWaterCommand>(&bytes);
331        assert!(
332            decoded.is_err(),
333            "AdvanceDenseBatch with empty key must fail to decode, got {decoded:?}",
334        );
335    }
336}