Expand description
EzRaft - A beginner-friendly Raft framework built on openraft
EzRaft runs your application on several machines at once and keeps their state identical, so the service survives losing some of them. You write two things - everything that makes the machines agree is handled internally:
- The application via
EzApp: request/response types, state, and business logic - Storage persistence via
EzStorage
§What a cluster does
A cluster is a set of nodes, each one a process running your application. Every node
keeps the same list of requests - the log - in the same order. A request is committed
once a majority of the nodes have stored it, which is the point at which it can no longer be
lost. Each node then applies committed requests to its own copy of your state, in log
order, so every node arrives at the same state. EzApp::apply is that step.
Because every node holds the whole state, any node can answer a read from memory
(EzRaft::read) without talking to anyone. Writes take the longer path: they go through
one node elected leader, and EzRaft::write finds it for you.
A snapshot is the state serialized. It lets a node that has fallen too far behind be caught up in one transfer instead of replaying the log, and it lets old log entries be deleted. EzRaft builds and installs snapshots on its own.
What matters for fault tolerance is that a majority survives: three nodes tolerate one failure, five tolerate two.
§Quick start
A complete key-value node. This is examples/kvstore.rs without its command line - run that
with cargo run --example kvstore to get a cluster going.
use std::collections::BTreeMap;
use std::io;
use async_trait::async_trait;
use ezraft::EzApp;
use ezraft::EzConfig;
use ezraft::EzRaft;
use ezraft::FileStorage;
use serde::Deserialize;
use serde::Serialize;
// 1. What a client asks the cluster to do. It travels the network and goes into
// the log, hence serde; openraft prints it in its logs, hence Display.
#[derive(Serialize, Deserialize, Debug, Clone, derive_more::Display)]
enum Request {
#[display("Set({key})")]
Set { key: String, value: String },
}
// 2. What `apply` hands back to whoever issued the write.
#[derive(Serialize, Deserialize, Debug, Clone)]
struct Response {
value: Option<String>,
}
// 3. The application *is* the replicated state. A snapshot is this struct
// serialized, so serde is all it takes.
#[derive(Default, Serialize, Deserialize)]
struct KvApp {
data: BTreeMap<String, String>,
}
#[async_trait]
impl EzApp for KvApp {
type Request = Request;
type Response = Response;
// Called once per committed entry, in log order, on every node.
async fn apply(&mut self, req: Request) -> Response {
match req {
Request::Set { key, value } => Response {
value: self.data.insert(key, value),
},
}
}
// Optional: answers `GET /api/read?key=...` from local state.
fn read(&self, key: &str) -> Option<serde_json::Value> {
self.data.get(key).map(|v| serde_json::Value::String(v.clone()))
}
}
#[tokio::main]
async fn main() -> io::Result<()> {
// 4. Where this node's state goes. One directory per node. `FileStorage`
// is the bundled `EzStorage`; read its caveats before keeping it.
let storage = FileStorage::new("./data/node1").await?;
// 5. The first node of a cluster creates it; every other node joins
// through any node already in it:
// EzRaft::join("127.0.0.1:8081", "127.0.0.1:8080", app, storage, config)
let raft = EzRaft::create("127.0.0.1:8080", KvApp::default(), storage, EzConfig::default()).await?;
raft.write(Request::Set {
key: "hello".to_string(),
value: "world".to_string(),
})
.await?;
println!("{:?}", raft.read(|app| app.data.get("hello").cloned()).await);
// 6. Serve the Raft RPCs peers need, plus the app API. This blocks;
// `tokio::spawn` it when the caller has other work to do.
raft.serve().await
}§Errors
Every fallible method returns std::io::Error, including the ones that fail for reasons
that have nothing to do with I/O. This is deliberate: a caller cannot usefully branch on the
difference. A write that finds no leader, one that cannot reach the leader, and one issued to
a stopped node all mean the same thing to an application – try again later – because
EzRaft::write already forwards to the leader on its own.
EzStorage is where a user’s own errors originate, and those are I/O errors already, so a
second error type would buy nothing and cost a concept. Code that does need to tell the cases
apart can reach the underlying openraft node and its typed errors through
EzRaft::inner.
Re-exports§
pub use app::EzApp;pub use config::EzConfig;pub use entry::EzEntry;pub use entry::EzLogId;pub use meta::EzMeta;pub use raft::EzRaft;pub use snapshot::EzSnapshot;pub use snapshot::EzSnapshotMeta;pub use storage::EzStorage;pub use storage::FileStorage;pub use storage::Loaded;pub use storage::Persist;pub use type_config::EzVote;pub use type_config::OpenRaftTypes;
Modules§
- app
- The application a cluster replicates
- config
- Configuration for EzRaft
- entry
- The Raft log entry
- meta
- Raft metadata the framework asks storage to keep
- network
- HTTP network layer for EzRaft
- raft
- Main EzRaft API
- server
- HTTP server for EzRaft
- snapshot
- The snapshot: a serialized
EzAppand where in the log it stands - storage
- Persistence: the
EzStoragecontract, the values it exchanges, and one implementation - type_
config - Type configuration for EzRaft
Traits§
- Raft
Type Config - Type configuration for customizing Raft components.