use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
pub mod actor;
pub mod actor_store;
pub mod catalog;
pub mod client;
pub mod group;
pub mod group_migrate;
pub mod join;
pub mod leave;
pub mod queue;
pub mod queue_autoscale;
pub mod raft;
pub mod saga_journal;
pub mod two_phase;
pub mod two_phase_journal;
pub use actor::{
ActorEnvelope, ActorId, ActorRef, ActorRegistration, ActorTypeId, DeliverAck, DirectoryUpdate,
MigrateReply, MigrateRequest, RegisterAck, ScaleReply, ScaleRequest, SpawnReply, SpawnRequest,
StopReply, StopRequest,
};
pub use actor_store::{
StoreCompareAndSetReply, StoreCompareAndSetRequest, StoreDeleteReply, StoreDeleteRequest,
StoreReplicateOp, StoreReplicateReply, StoreReplicateRequest, StoreSetReply, StoreSetRequest,
};
pub use catalog::{CatalogAddRequest, CatalogAddResponse, CatalogCommand, CatalogRejection};
pub use client::{ClientRequest, ClientResponse};
pub use group::GroupPeerEnvelope;
pub use group_migrate::{
GroupMigrateReply, GroupMigrateRequest, GroupMigrationBundle, GroupMigrationHardState,
GroupMigrationSnapshot, GroupMigrationSnapshotMeta,
};
pub use join::{JoinRejection, JoinRequest, JoinResponse, PeerBook, PeerEntry};
pub use leave::{LeaveRejection, LeaveRequest, LeaveResponse};
pub use queue::{
QueueAckBatchReply, QueueAckBatchRequest, QueueAckReply, QueueAckRequest, QueueBatchEnqueueJob,
QueueEnqueueBatchReply, QueueEnqueueBatchRequest, QueueEnqueueReply, QueueEnqueueRequest,
QueueJobLifecycleWire, QueueJobStatusReply, QueueJobStatusRequest, QueueLeaseReply,
QueueLeaseRequest, QueueLeasedJobWire, QueueMetricsReply, QueueMetricsRequest, QueueNackReply,
QueueNackRequest, QueueReplicateOp, QueueReplicateReply, QueueReplicateRequest,
QueueRequeueDeadLetterReply, QueueRequeueDeadLetterRequest, RecurringScheduleWire,
};
pub use queue_autoscale::{
AutoscalePolicyWire, MembershipAutoscalePolicyWire, QueueAutoscalePolicyCommand,
};
pub use raft::{
AppendEntries, AppendEntriesReply, EntryPayload, InstallSnapshot, InstallSnapshotReply,
LogEntry, Membership, RaftRpc, RaftRpcReply, RequestVote, RequestVoteReply,
};
pub use saga_journal::SagaJournalCommand;
pub use two_phase::{TwoPhaseAbortCommand, TwoPhasePrepareCommand};
pub use two_phase_journal::TwoPhaseJournalCommand;
pub const PROTOCOL_VERSION: u32 = 1;
pub const MIN_COMPATIBLE_PROTOCOL_VERSION: u32 = 1;
#[must_use]
pub fn protocol_version_compatible(got: u32) -> bool {
got >= MIN_COMPATIBLE_PROTOCOL_VERSION && got <= PROTOCOL_VERSION
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct NodeId(pub u64);
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize,
)]
pub struct Term(pub u64);
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize,
)]
pub struct LogIndex(pub u64);
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize,
)]
pub struct Round(pub u64);
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize,
)]
pub struct LogId {
pub term: Term,
pub index: LogIndex,
}
impl Term {
pub const ZERO: Term = Term(0);
#[must_use]
pub fn next(self) -> Term {
Term(self.0 + 1)
}
}
impl LogIndex {
pub const ZERO: LogIndex = LogIndex(0);
#[must_use]
pub fn next(self) -> LogIndex {
LogIndex(self.0 + 1)
}
}
impl Round {
pub const ZERO: Round = Round(0);
#[must_use]
pub fn next(self) -> Round {
Round(self.0 + 1)
}
}
impl LogId {
pub const ZERO: LogId = LogId {
term: Term::ZERO,
index: LogIndex::ZERO,
};
#[must_use]
pub fn new(term: Term, index: LogIndex) -> Self {
Self { term, index }
}
}
pub const WIRE_CODEC: &str = if cfg!(feature = "json-wire") {
"json"
} else {
"postcard"
};
#[derive(Debug, thiserror::Error)]
pub enum CodecError {
#[error("wire encode failed: {0}")]
Encode(String),
#[error("wire decode failed: {0}")]
Decode(String),
}
#[cfg(not(feature = "json-wire"))]
pub fn encode<T: Serialize>(value: &T) -> Result<Vec<u8>, CodecError> {
postcard::to_stdvec(value).map_err(|e| CodecError::Encode(e.to_string()))
}
#[cfg(not(feature = "json-wire"))]
pub fn decode<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, CodecError> {
postcard::from_bytes(bytes).map_err(|e| CodecError::Decode(e.to_string()))
}
#[cfg(feature = "json-wire")]
pub fn encode<T: Serialize>(value: &T) -> Result<Vec<u8>, CodecError> {
serde_json::to_vec(value).map_err(|e| CodecError::Encode(e.to_string()))
}
#[cfg(feature = "json-wire")]
pub fn decode<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, CodecError> {
serde_json::from_slice(bytes).map_err(|e| CodecError::Decode(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn term_and_index_advance() {
assert_eq!(Term::ZERO.next(), Term(1));
assert_eq!(LogIndex::ZERO.next(), LogIndex(1));
}
#[test]
fn roundtrip_log_entry() {
let entry = LogEntry {
term: Term(3),
index: LogIndex(7),
payload: EntryPayload::Command(vec![1, 2, 3]),
};
let bytes = encode(&entry).expect("encode");
let back: LogEntry = decode(&bytes).expect("decode");
assert_eq!(entry, back);
}
#[test]
fn roundtrip_saga_journal_entry() {
let entry = LogEntry {
term: Term(2),
index: LogIndex(4),
payload: EntryPayload::SagaJournal(SagaJournalCommand {
saga_id: b"saga-1".to_vec(),
record: vec![1, 2, 3],
}),
};
let bytes = encode(&entry).expect("encode");
let back: LogEntry = decode(&bytes).expect("decode");
assert_eq!(entry, back);
}
#[test]
fn roundtrip_two_phase_prepare_entry() {
let entry = LogEntry {
term: Term(2),
index: LogIndex(5),
payload: EntryPayload::TwoPhasePrepare(TwoPhasePrepareCommand {
tx_id: b"tx".to_vec(),
route_key: b"key".to_vec(),
command: vec![1, 2],
prepared_at_ms: 0,
}),
};
let bytes = encode(&entry).expect("encode");
let back: LogEntry = decode(&bytes).expect("decode");
assert_eq!(entry, back);
}
#[test]
fn roundtrip_raft_rpc() {
let rpc = RaftRpc::AppendEntries(AppendEntries {
term: Term(2),
leader_id: NodeId(1),
prev_log: LogId::new(Term(1), LogIndex(4)),
entries: vec![LogEntry {
term: Term(2),
index: LogIndex(5),
payload: EntryPayload::Noop,
}],
leader_commit: LogIndex(4),
round: Round(7),
});
let bytes = encode(&rpc).expect("encode");
let back: RaftRpc = decode(&bytes).expect("decode");
assert_eq!(rpc, back);
}
#[test]
fn log_id_orders_by_term_then_index() {
assert!(LogId::new(Term(2), LogIndex(1)) > LogId::new(Term(1), LogIndex(9)));
assert!(LogId::new(Term(1), LogIndex(5)) > LogId::new(Term(1), LogIndex(4)));
assert_eq!(
LogEntry {
term: Term(3),
index: LogIndex(7),
payload: EntryPayload::Noop
}
.id(),
LogId::new(Term(3), LogIndex(7))
);
}
#[test]
fn decode_rejects_garbage() {
let err = decode::<LogEntry>(&[0xff, 0xff, 0xff, 0xff]);
assert!(err.is_err());
}
#[test]
fn protocol_version_compatible_accepts_current_and_min() {
assert!(protocol_version_compatible(PROTOCOL_VERSION));
assert!(protocol_version_compatible(MIN_COMPATIBLE_PROTOCOL_VERSION));
assert!(!protocol_version_compatible(0));
assert!(!protocol_version_compatible(PROTOCOL_VERSION + 1));
}
#[test]
fn wire_codec_matches_build_feature() {
if cfg!(feature = "json-wire") {
assert_eq!(WIRE_CODEC, "json");
let bytes = encode(&RequestVote {
term: Term(1),
candidate_id: NodeId(2),
last_log: LogId::ZERO,
pre_vote: true,
})
.unwrap();
let text = String::from_utf8(bytes).unwrap();
assert!(text.contains("candidate_id"), "json body: {text}");
} else {
assert_eq!(WIRE_CODEC, "postcard");
}
}
}