1use serde::de::DeserializeOwned;
8use serde::{Deserialize, Serialize};
9
10pub mod actor;
11pub mod actor_store;
12pub mod catalog;
13pub mod client;
14pub mod group;
15pub mod group_migrate;
16pub mod join;
17pub mod leave;
18pub mod queue;
19pub mod queue_autoscale;
20pub mod raft;
21pub mod saga_journal;
22pub mod two_phase;
23pub mod two_phase_journal;
24
25pub use actor::{
26 ActorEnvelope, ActorId, ActorRef, ActorRegistration, ActorTypeId, DeliverAck, DirectoryUpdate,
27 MigrateReply, MigrateRequest, RegisterAck, ScaleReply, ScaleRequest, SpawnReply, SpawnRequest,
28 StopReply, StopRequest,
29};
30pub use actor_store::{
31 StoreCompareAndSetReply, StoreCompareAndSetRequest, StoreDeleteReply, StoreDeleteRequest,
32 StoreReplicateOp, StoreReplicateReply, StoreReplicateRequest, StoreSetReply, StoreSetRequest,
33};
34pub use catalog::{CatalogAddRequest, CatalogAddResponse, CatalogCommand, CatalogRejection};
35pub use client::{ClientRequest, ClientResponse};
36pub use group::GroupPeerEnvelope;
37pub use group_migrate::{
38 GroupMigrateReply, GroupMigrateRequest, GroupMigrationBundle, GroupMigrationHardState,
39 GroupMigrationSnapshot, GroupMigrationSnapshotMeta,
40};
41pub use join::{JoinRejection, JoinRequest, JoinResponse, PeerBook, PeerEntry};
42pub use leave::{LeaveRejection, LeaveRequest, LeaveResponse};
43pub use queue::{
44 QueueAckReply, QueueAckRequest, QueueEnqueueReply, QueueEnqueueRequest, QueueJobLifecycleWire,
45 QueueJobStatusReply, QueueJobStatusRequest, QueueLeaseReply, QueueLeaseRequest,
46 QueueLeasedJobWire, QueueMetricsReply, QueueMetricsRequest, QueueNackReply, QueueNackRequest,
47 QueueReplicateOp, QueueReplicateReply, QueueReplicateRequest,
48};
49pub use queue_autoscale::{
50 AutoscalePolicyWire, MembershipAutoscalePolicyWire, QueueAutoscalePolicyCommand,
51};
52pub use raft::{
53 AppendEntries, AppendEntriesReply, EntryPayload, InstallSnapshot, InstallSnapshotReply,
54 LogEntry, Membership, RaftRpc, RaftRpcReply, RequestVote, RequestVoteReply,
55};
56pub use saga_journal::SagaJournalCommand;
57pub use two_phase::{TwoPhaseAbortCommand, TwoPhasePrepareCommand};
58pub use two_phase_journal::TwoPhaseJournalCommand;
59
60pub const PROTOCOL_VERSION: u32 = 1;
62
63pub const MIN_COMPATIBLE_PROTOCOL_VERSION: u32 = 1;
65
66#[must_use]
68pub fn protocol_version_compatible(got: u32) -> bool {
69 got >= MIN_COMPATIBLE_PROTOCOL_VERSION && got <= PROTOCOL_VERSION
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
74pub struct NodeId(pub u64);
75
76#[derive(
78 Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize,
79)]
80pub struct Term(pub u64);
81
82#[derive(
84 Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize,
85)]
86pub struct LogIndex(pub u64);
87
88#[derive(
91 Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize,
92)]
93pub struct Round(pub u64);
94
95#[derive(
100 Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize,
101)]
102pub struct LogId {
103 pub term: Term,
105 pub index: LogIndex,
107}
108
109impl Term {
110 pub const ZERO: Term = Term(0);
112
113 #[must_use]
115 pub fn next(self) -> Term {
116 Term(self.0 + 1)
117 }
118}
119
120impl LogIndex {
121 pub const ZERO: LogIndex = LogIndex(0);
123
124 #[must_use]
126 pub fn next(self) -> LogIndex {
127 LogIndex(self.0 + 1)
128 }
129}
130
131impl Round {
132 pub const ZERO: Round = Round(0);
134
135 #[must_use]
137 pub fn next(self) -> Round {
138 Round(self.0 + 1)
139 }
140}
141
142impl LogId {
143 pub const ZERO: LogId = LogId {
145 term: Term::ZERO,
146 index: LogIndex::ZERO,
147 };
148
149 #[must_use]
151 pub fn new(term: Term, index: LogIndex) -> Self {
152 Self { term, index }
153 }
154}
155
156pub const WIRE_CODEC: &str = if cfg!(feature = "json-wire") {
160 "json"
161} else {
162 "postcard"
163};
164
165#[derive(Debug, thiserror::Error)]
167pub enum CodecError {
168 #[error("wire encode failed: {0}")]
170 Encode(String),
171 #[error("wire decode failed: {0}")]
173 Decode(String),
174}
175
176#[cfg(not(feature = "json-wire"))]
184pub fn encode<T: Serialize>(value: &T) -> Result<Vec<u8>, CodecError> {
185 postcard::to_stdvec(value).map_err(|e| CodecError::Encode(e.to_string()))
186}
187
188#[cfg(not(feature = "json-wire"))]
193pub fn decode<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, CodecError> {
194 postcard::from_bytes(bytes).map_err(|e| CodecError::Decode(e.to_string()))
195}
196
197#[cfg(feature = "json-wire")]
202pub fn encode<T: Serialize>(value: &T) -> Result<Vec<u8>, CodecError> {
203 serde_json::to_vec(value).map_err(|e| CodecError::Encode(e.to_string()))
204}
205
206#[cfg(feature = "json-wire")]
211pub fn decode<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, CodecError> {
212 serde_json::from_slice(bytes).map_err(|e| CodecError::Decode(e.to_string()))
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn term_and_index_advance() {
221 assert_eq!(Term::ZERO.next(), Term(1));
222 assert_eq!(LogIndex::ZERO.next(), LogIndex(1));
223 }
224
225 #[test]
226 fn roundtrip_log_entry() {
227 let entry = LogEntry {
228 term: Term(3),
229 index: LogIndex(7),
230 payload: EntryPayload::Command(vec![1, 2, 3]),
231 };
232 let bytes = encode(&entry).expect("encode");
233 let back: LogEntry = decode(&bytes).expect("decode");
234 assert_eq!(entry, back);
235 }
236
237 #[test]
238 fn roundtrip_saga_journal_entry() {
239 let entry = LogEntry {
240 term: Term(2),
241 index: LogIndex(4),
242 payload: EntryPayload::SagaJournal(SagaJournalCommand {
243 saga_id: b"saga-1".to_vec(),
244 record: vec![1, 2, 3],
245 }),
246 };
247 let bytes = encode(&entry).expect("encode");
248 let back: LogEntry = decode(&bytes).expect("decode");
249 assert_eq!(entry, back);
250 }
251
252 #[test]
253 fn roundtrip_two_phase_prepare_entry() {
254 let entry = LogEntry {
255 term: Term(2),
256 index: LogIndex(5),
257 payload: EntryPayload::TwoPhasePrepare(TwoPhasePrepareCommand {
258 tx_id: b"tx".to_vec(),
259 route_key: b"key".to_vec(),
260 command: vec![1, 2],
261 prepared_at_ms: 0,
262 }),
263 };
264 let bytes = encode(&entry).expect("encode");
265 let back: LogEntry = decode(&bytes).expect("decode");
266 assert_eq!(entry, back);
267 }
268
269 #[test]
270 fn roundtrip_raft_rpc() {
271 let rpc = RaftRpc::AppendEntries(AppendEntries {
272 term: Term(2),
273 leader_id: NodeId(1),
274 prev_log: LogId::new(Term(1), LogIndex(4)),
275 entries: vec![LogEntry {
276 term: Term(2),
277 index: LogIndex(5),
278 payload: EntryPayload::Noop,
279 }],
280 leader_commit: LogIndex(4),
281 round: Round(7),
282 });
283 let bytes = encode(&rpc).expect("encode");
284 let back: RaftRpc = decode(&bytes).expect("decode");
285 assert_eq!(rpc, back);
286 }
287
288 #[test]
289 fn log_id_orders_by_term_then_index() {
290 assert!(LogId::new(Term(2), LogIndex(1)) > LogId::new(Term(1), LogIndex(9)));
293 assert!(LogId::new(Term(1), LogIndex(5)) > LogId::new(Term(1), LogIndex(4)));
294 assert_eq!(
295 LogEntry {
296 term: Term(3),
297 index: LogIndex(7),
298 payload: EntryPayload::Noop
299 }
300 .id(),
301 LogId::new(Term(3), LogIndex(7))
302 );
303 }
304
305 #[test]
306 fn decode_rejects_garbage() {
307 let err = decode::<LogEntry>(&[0xff, 0xff, 0xff, 0xff]);
308 assert!(err.is_err());
309 }
310
311 #[test]
312 fn protocol_version_compatible_accepts_current_and_min() {
313 assert!(protocol_version_compatible(PROTOCOL_VERSION));
314 assert!(protocol_version_compatible(MIN_COMPATIBLE_PROTOCOL_VERSION));
315 assert!(!protocol_version_compatible(0));
316 assert!(!protocol_version_compatible(PROTOCOL_VERSION + 1));
317 }
318
319 #[test]
320 fn wire_codec_matches_build_feature() {
321 if cfg!(feature = "json-wire") {
323 assert_eq!(WIRE_CODEC, "json");
324 let bytes = encode(&RequestVote {
326 term: Term(1),
327 candidate_id: NodeId(2),
328 last_log: LogId::ZERO,
329 pre_vote: true,
330 })
331 .unwrap();
332 let text = String::from_utf8(bytes).unwrap();
333 assert!(text.contains("candidate_id"), "json body: {text}");
334 } else {
335 assert_eq!(WIRE_CODEC, "postcard");
336 }
337 }
338}