# EzRaft
[](https://github.com/drmingdrmer/ezraft/actions/workflows/ci.yaml)
[](https://crates.io/crates/ezraft)
[](https://docs.rs/ezraft)
[](https://github.com/drmingdrmer/ezraft#license)
A beginner-friendly Raft consensus framework built on
[OpenRaft](https://github.com/databendlabs/openraft).
Run your application on several machines at once, all holding the same state, so the
service survives losing some of them. That is what [Raft](https://raft.github.io/) is
for, and EzRaft reduces it to two traits: `EzApp` holds your state and applies requests
to it, `EzStorage` puts bytes on disk. Elections, replication, membership, snapshots and
the transport between machines are handled internally.
Where to draw that line is the point of the crate: EzRaft is a search for the smallest
API that is still enough to build a distributed key-value store on Raft.
The way to judge an API is to build with it, so the example here is a whole working
service. If a part of it feels heavier than the job deserves, that is worth reporting.
- **Two methods**: `apply()` performs a write, `read()` answers a read - each with a
request and a response type you choose. That is the whole application interface.
- **Your own types**: requests, responses and state are your structs. Serde moves them
between nodes; nothing here deals in byte vectors.
- **Storage included**: `FileStorage` persists to disk out of the box. Writing your own
is three methods.
- **Networking included**: every node serves the Raft RPCs its peers need and an HTTP
API for your app. There is no transport to write.
## Example
A replicated key-value service, whole. First the application: the two methods above,
and the four types they carry.
```rust
#[derive(Serialize, Deserialize, Debug, Clone, derive_more::Display)]
enum Request {
#[display("Set({key})")]
Set { key: String, value: String },
}
/// The application *is* the replicated state: a snapshot is this struct, serialized.
#[derive(Default, Serialize, Deserialize)]
struct KvApp {
data: BTreeMap<String, String>,
}
#[async_trait]
impl EzApp for KvApp {
type Request = Request;
type Response = Option<String>;
/// Called once per committed entry, in log order, on every node.
async fn apply(&mut self, req: Request) -> Option<String> {
match req {
Request::Set { key, value } => self.data.insert(key, value),
}
}
type ReadRequest = String;
type ReadResponse = Option<String>;
/// Answered from local state: no consensus round, no log entry.
fn read(&self, key: String) -> Option<String> {
self.data.get(&key).cloned()
}
}
```
Then `main`, which is where the cluster comes from. `FileStorage` is the bundled
`EzStorage`, so there is nothing else to implement:
```rust
/// One binary, run once per machine: `kvstore <own-addr> [addr of a node already in it]`
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut args = std::env::args().skip(1);
let addr = args.next().expect("usage: kvstore <addr> [seed_addr]");
let seed = args.next();
let storage = FileStorage::new(format!("./data/{addr}")).await?;
let (app, config) = (KvApp::default(), EzConfig::default());
// The first node creates the cluster; every other one joins through a node in it
// and is given an id, the membership, and the log so far.
let raft = match seed {
None => EzRaft::create(addr, app, storage, config).await?,
Some(seed) => EzRaft::join(addr, seed, app, storage, config).await?,
};
// The Raft RPCs peers need, plus the app API below. Runs until shutdown.
raft.serve().await
}
```
Three of those form a cluster that survives losing any one of them:
```bash
kvstore 127.0.0.1:8080 # creates it
kvstore 127.0.0.1:8081 127.0.0.1:8080 # joins
kvstore 127.0.0.1:8082 127.0.0.1:8080 # joins
```
## Drive it
```bash
# A write goes through the log: replicated and committed before it answers.
curl -X POST 127.0.0.1:8080/api/write -H 'Content-Type: application/json' \
-d '{"Set": {"key": "hello", "value": "world"}}'
# null - Set answers with the value it replaced, if there was one
# A read is answered from that node's own memory: no consensus round, no log entry.
curl -X POST 127.0.0.1:8082/api/read -H 'Content-Type: application/json' -d '"hello"'
# "world" - from a node you never wrote to
curl 127.0.0.1:8080/api/metrics # leader, term, log index, membership
```
## Status
**Experimental.** The API is the thing being searched for, so it changes until the
crate stabilizes. A real service built on it is the feedback the search needs most -
pin the version, and read the CHANGELOG before upgrading.
**Next phase: Stable API.** Once the design exploration matures, EzRaft will provide a
stable API with well-considered abstractions - exposing what users need while hiding
unnecessary complexity.
## Documentation
- [Guide](docs/guide.md) - run a cluster, write a service against it, and the API,
configuration and HTTP endpoints that make it up
- [API documentation](https://docs.rs/ezraft)
- [`examples/kvstore.rs`](examples/kvstore.rs) - a complete key-value service
- [CHANGELOG](CHANGELOG.md)
## License
Licensed under either of [Apache-2.0](LICENSE-APACHE) or [MIT](LICENSE-MIT), at your option.