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}