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 QueueAckBatchReply, QueueAckBatchRequest, QueueAckReply, QueueAckRequest, QueueBatchEnqueueJob,
45 QueueEnqueueBatchReply, QueueEnqueueBatchRequest, QueueEnqueueReply, QueueEnqueueRequest,
46 QueueJobLifecycleWire, QueueJobStatusReply, QueueJobStatusRequest, QueueLeaseReply,
47 QueueLeaseRequest, QueueLeasedJobWire, QueueMetricsReply, QueueMetricsRequest, QueueNackReply,
48 QueueNackRequest, QueueReplicateOp, QueueReplicateReply, QueueReplicateRequest,
49 QueueRequeueDeadLetterReply, QueueRequeueDeadLetterRequest, RecurringScheduleWire,
50};
51pub use queue_autoscale::{
52 AutoscalePolicyWire, MembershipAutoscalePolicyWire, QueueAutoscalePolicyCommand,
53};
54pub use raft::{
55 AppendEntries, AppendEntriesReply, EntryPayload, InstallSnapshot, InstallSnapshotReply,
56 LogEntry, Membership, RaftRpc, RaftRpcReply, RequestVote, RequestVoteReply,
57};
58pub use saga_journal::SagaJournalCommand;
59pub use two_phase::{TwoPhaseAbortCommand, TwoPhasePrepareCommand};
60pub use two_phase_journal::TwoPhaseJournalCommand;
61
62pub const PROTOCOL_VERSION: u32 = 1;
64
65pub const MIN_COMPATIBLE_PROTOCOL_VERSION: u32 = 1;
67
68#[must_use]
70pub fn protocol_version_compatible(got: u32) -> bool {
71 got >= MIN_COMPATIBLE_PROTOCOL_VERSION && got <= PROTOCOL_VERSION
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
76pub struct NodeId(pub u64);
77
78#[derive(
80 Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize,
81)]
82pub struct Term(pub u64);
83
84#[derive(
86 Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize,
87)]
88pub struct LogIndex(pub u64);
89
90#[derive(
93 Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize,
94)]
95pub struct Round(pub u64);
96
97#[derive(
102 Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize,
103)]
104pub struct LogId {
105 pub term: Term,
107 pub index: LogIndex,
109}
110
111impl Term {
112 pub const ZERO: Term = Term(0);
114
115 #[must_use]
117 pub fn next(self) -> Term {
118 Term(self.0 + 1)
119 }
120}
121
122impl LogIndex {
123 pub const ZERO: LogIndex = LogIndex(0);
125
126 #[must_use]
128 pub fn next(self) -> LogIndex {
129 LogIndex(self.0 + 1)
130 }
131}
132
133impl Round {
134 pub const ZERO: Round = Round(0);
136
137 #[must_use]
139 pub fn next(self) -> Round {
140 Round(self.0 + 1)
141 }
142}
143
144impl LogId {
145 pub const ZERO: LogId = LogId {
147 term: Term::ZERO,
148 index: LogIndex::ZERO,
149 };
150
151 #[must_use]
153 pub fn new(term: Term, index: LogIndex) -> Self {
154 Self { term, index }
155 }
156}
157
158pub const WIRE_CODEC: &str = if cfg!(feature = "json-wire") {
162 "json"
163} else {
164 "postcard"
165};
166
167#[derive(Debug, thiserror::Error)]
169pub enum CodecError {
170 #[error("wire encode failed: {0}")]
172 Encode(String),
173 #[error("wire decode failed: {0}")]
175 Decode(String),
176}
177
178#[cfg(not(feature = "json-wire"))]
186pub fn encode<T: Serialize>(value: &T) -> Result<Vec<u8>, CodecError> {
187 postcard::to_stdvec(value).map_err(|e| CodecError::Encode(e.to_string()))
188}
189
190#[cfg(not(feature = "json-wire"))]
195pub fn decode<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, CodecError> {
196 postcard::from_bytes(bytes).map_err(|e| CodecError::Decode(e.to_string()))
197}
198
199#[cfg(feature = "json-wire")]
204pub fn encode<T: Serialize>(value: &T) -> Result<Vec<u8>, CodecError> {
205 serde_json::to_vec(value).map_err(|e| CodecError::Encode(e.to_string()))
206}
207
208#[cfg(feature = "json-wire")]
213pub fn decode<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, CodecError> {
214 serde_json::from_slice(bytes).map_err(|e| CodecError::Decode(e.to_string()))
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn term_and_index_advance() {
223 assert_eq!(Term::ZERO.next(), Term(1));
224 assert_eq!(LogIndex::ZERO.next(), LogIndex(1));
225 }
226
227 #[test]
228 fn roundtrip_log_entry() {
229 let entry = LogEntry {
230 term: Term(3),
231 index: LogIndex(7),
232 payload: EntryPayload::Command(vec![1, 2, 3]),
233 };
234 let bytes = encode(&entry).expect("encode");
235 let back: LogEntry = decode(&bytes).expect("decode");
236 assert_eq!(entry, back);
237 }
238
239 #[test]
240 fn roundtrip_saga_journal_entry() {
241 let entry = LogEntry {
242 term: Term(2),
243 index: LogIndex(4),
244 payload: EntryPayload::SagaJournal(SagaJournalCommand {
245 saga_id: b"saga-1".to_vec(),
246 record: vec![1, 2, 3],
247 }),
248 };
249 let bytes = encode(&entry).expect("encode");
250 let back: LogEntry = decode(&bytes).expect("decode");
251 assert_eq!(entry, back);
252 }
253
254 #[test]
255 fn roundtrip_two_phase_prepare_entry() {
256 let entry = LogEntry {
257 term: Term(2),
258 index: LogIndex(5),
259 payload: EntryPayload::TwoPhasePrepare(TwoPhasePrepareCommand {
260 tx_id: b"tx".to_vec(),
261 route_key: b"key".to_vec(),
262 command: vec![1, 2],
263 prepared_at_ms: 0,
264 }),
265 };
266 let bytes = encode(&entry).expect("encode");
267 let back: LogEntry = decode(&bytes).expect("decode");
268 assert_eq!(entry, back);
269 }
270
271 #[test]
272 fn roundtrip_raft_rpc() {
273 let rpc = RaftRpc::AppendEntries(AppendEntries {
274 term: Term(2),
275 leader_id: NodeId(1),
276 prev_log: LogId::new(Term(1), LogIndex(4)),
277 entries: vec![LogEntry {
278 term: Term(2),
279 index: LogIndex(5),
280 payload: EntryPayload::Noop,
281 }],
282 leader_commit: LogIndex(4),
283 round: Round(7),
284 });
285 let bytes = encode(&rpc).expect("encode");
286 let back: RaftRpc = decode(&bytes).expect("decode");
287 assert_eq!(rpc, back);
288 }
289
290 #[test]
291 fn log_id_orders_by_term_then_index() {
292 assert!(LogId::new(Term(2), LogIndex(1)) > LogId::new(Term(1), LogIndex(9)));
295 assert!(LogId::new(Term(1), LogIndex(5)) > LogId::new(Term(1), LogIndex(4)));
296 assert_eq!(
297 LogEntry {
298 term: Term(3),
299 index: LogIndex(7),
300 payload: EntryPayload::Noop
301 }
302 .id(),
303 LogId::new(Term(3), LogIndex(7))
304 );
305 }
306
307 #[test]
308 fn decode_rejects_garbage() {
309 let err = decode::<LogEntry>(&[0xff, 0xff, 0xff, 0xff]);
310 assert!(err.is_err());
311 }
312
313 #[test]
314 fn protocol_version_compatible_accepts_current_and_min() {
315 assert!(protocol_version_compatible(PROTOCOL_VERSION));
316 assert!(protocol_version_compatible(MIN_COMPATIBLE_PROTOCOL_VERSION));
317 assert!(!protocol_version_compatible(0));
318 assert!(!protocol_version_compatible(PROTOCOL_VERSION + 1));
319 }
320
321 #[test]
322 fn wire_codec_matches_build_feature() {
323 if cfg!(feature = "json-wire") {
325 assert_eq!(WIRE_CODEC, "json");
326 let bytes = encode(&RequestVote {
328 term: Term(1),
329 candidate_id: NodeId(2),
330 last_log: LogId::ZERO,
331 pre_vote: true,
332 })
333 .unwrap();
334 let text = String::from_utf8(bytes).unwrap();
335 assert!(text.contains("candidate_id"), "json body: {text}");
336 } else {
337 assert_eq!(WIRE_CODEC, "postcard");
338 }
339 }
340}