Skip to main content

Crate ezraft

Crate ezraft 

Source
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 EzApp and where in the log it stands
storage
Persistence: the EzStorage contract, the values it exchanges, and one implementation
type_config
Type configuration for EzRaft

Traits§

RaftTypeConfig
Type configuration for customizing Raft components.