crafty_core/state_machine.rs
1//! Application state-machine API (state-machine).
2//!
3//! A cluster replicates an opaque log; the *application* decides what those
4//! entries mean by implementing [`StateMachine`]. The Raft core commits and
5//! orders entries, then the runtime feeds each committed command to
6//! [`StateMachine::apply`] exactly once, in index order (architecture-style applier loop).
7//!
8//! ## Encode/decode glue (state-machine)
9//!
10//! state-machine called for macros to generate `Encode`/`Decode` glue and to check
11//! that command types are *owned* and *clone-safe* for replication. In this
12//! stack that glue is already provided generically by `serde` + `postcard`, so
13//! instead of a bespoke derive we expose the [`Command`] and [`Query`] marker
14//! traits with **blanket implementations** over any `serde` type that also
15//! satisfies the replication bounds. The bounds (`Clone + Send + 'static`) are
16//! exactly the "owned & clone-safe" compile-time check state-machine asked for โ a
17//! type borrowing a lifetime, or one that is not `Clone`, simply will not
18//! satisfy [`Command`] and the code will not compile.
19
20use crafty_proto::{CodecError, LogIndex, decode, encode};
21use serde::Serialize;
22use serde::de::DeserializeOwned;
23
24/// A replicated command applied to the [`StateMachine`].
25///
26/// Commands are serialized into the Raft log, shipped to peers, and later
27/// decoded and applied, so they must be self-owned (`'static`), `Clone`able
28/// (a leader may retry replication), and `Send` (they cross task/actor
29/// boundaries). Any type meeting those bounds plus `serde` gets a [`Command`]
30/// implementation for free via the blanket impl below โ derive
31/// `#[derive(Clone, Serialize, Deserialize)]` and you are done.
32pub trait Command: Clone + Send + 'static {
33 /// Encode this command to `postcard` bytes for the log/wire.
34 ///
35 /// # Errors
36 /// Returns [`CodecError`] if serialization fails.
37 fn to_bytes(&self) -> Result<Vec<u8>, CodecError>;
38
39 /// Decode a command from `postcard` bytes read back from the log/wire.
40 ///
41 /// # Errors
42 /// Returns [`CodecError`] if deserialization fails.
43 fn from_bytes(bytes: &[u8]) -> Result<Self, CodecError>
44 where
45 Self: Sized;
46}
47
48impl<T> Command for T
49where
50 T: Clone + Send + 'static + Serialize + DeserializeOwned,
51{
52 fn to_bytes(&self) -> Result<Vec<u8>, CodecError> {
53 encode(self)
54 }
55
56 fn from_bytes(bytes: &[u8]) -> Result<Self, CodecError> {
57 decode(bytes)
58 }
59}
60
61/// A read-only query served by the [`StateMachine`].
62///
63/// Queries never enter the log; they are answered locally after the `ReadIndex`
64/// protocol confirms the leader is current (read-consistency). They still cross task
65/// boundaries and may be sent to a remote leader, hence `Send + 'static` +
66/// `serde`. Unlike a [`Command`], a query need not be `Clone`.
67pub trait Query: Send + 'static {
68 /// Encode this query to `postcard` bytes for the wire.
69 ///
70 /// # Errors
71 /// Returns [`CodecError`] if serialization fails.
72 fn to_bytes(&self) -> Result<Vec<u8>, CodecError>;
73
74 /// Decode a query from `postcard` bytes.
75 ///
76 /// # Errors
77 /// Returns [`CodecError`] if deserialization fails.
78 fn from_bytes(bytes: &[u8]) -> Result<Self, CodecError>
79 where
80 Self: Sized;
81}
82
83impl<T> Query for T
84where
85 T: Send + 'static + Serialize + DeserializeOwned,
86{
87 fn to_bytes(&self) -> Result<Vec<u8>, CodecError> {
88 encode(self)
89 }
90
91 fn from_bytes(bytes: &[u8]) -> Result<Self, CodecError> {
92 decode(bytes)
93 }
94}
95
96/// The user-defined, deterministic application state machine (state-machine).
97///
98/// Implementations must be **deterministic**: applying the same sequence of
99/// commands from the same snapshot must always yield the same state and the
100/// same per-command [`Response`](StateMachine::Response). This is what lets a
101/// lagging follower rebuild identical state purely from the replicated log, and
102/// what lets the deterministic simulator (testing-strategy) reproduce runs. Avoid wall
103/// clocks, RNGs, and external I/O inside [`apply`](StateMachine::apply); feed
104/// any such inputs in through the command instead.
105pub trait StateMachine: Send + 'static {
106 /// The command type applied by [`apply`](StateMachine::apply).
107 type Command: Command;
108 /// The read-only query type answered by [`query`](StateMachine::query).
109 type Query: Query;
110 /// The value returned to the client from an `apply` or `query`.
111 type Response: Send + 'static + Serialize + DeserializeOwned;
112 /// The error type surfaced when a command or query cannot be handled.
113 type Error: std::error::Error + Send + Sync + 'static;
114
115 /// Apply a committed command at log `index`, mutating state and returning a
116 /// response for the client.
117 ///
118 /// The runtime calls this exactly once per committed command, in ascending
119 /// index order. `index` is provided so implementations can persist an
120 /// applied-through watermark for idempotent external side effects
121 /// (actor-state-redis), though the in-log state itself needs no such bookkeeping.
122 ///
123 /// # Errors
124 /// Returns [`Self::Error`](StateMachine::Error) if the command is invalid
125 /// for the current state. Note that a returned error does **not** roll back
126 /// the log entry โ it is reported to the client while the command remains
127 /// committed, so implementations should validate before mutating.
128 fn apply(
129 &mut self,
130 index: LogIndex,
131 command: &Self::Command,
132 ) -> Result<Self::Response, Self::Error>;
133
134 /// Answer a read-only query against the current applied state.
135 ///
136 /// Must not mutate state. Linearizability is guaranteed by the caller via
137 /// `ReadIndex` (read-consistency), not by this method.
138 ///
139 /// # Errors
140 /// Returns [`Self::Error`](StateMachine::Error) if the query is invalid.
141 fn query(&self, query: &Self::Query) -> Result<Self::Response, Self::Error>;
142
143 /// Serialize the entire machine state into a snapshot image for log
144 /// compaction (Raft ยง7). The bytes are opaque to the core; only
145 /// [`restore`](StateMachine::restore) interprets them.
146 ///
147 /// # Errors
148 /// Returns [`Self::Error`](StateMachine::Error) if the state cannot be
149 /// serialized.
150 fn snapshot(&self) -> Result<Vec<u8>, Self::Error>;
151
152 /// Replace the machine state with the one encoded in `snapshot`, discarding
153 /// any current state. Called when a follower installs a leader snapshot or
154 /// a node restarts from disk.
155 ///
156 /// # Errors
157 /// Returns [`Self::Error`](StateMachine::Error) if the snapshot is
158 /// malformed.
159 fn restore(&mut self, snapshot: &[u8]) -> Result<(), Self::Error>;
160}