Skip to main content

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}