1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
//! 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.
//!
//! ```no_run
//! 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`](raft::EzRaft::inner).
// Re-export public API
pub use EzApp;
pub use EzConfig;
pub use EzEntry;
pub use EzLogId;
pub use EzMeta;
pub use RaftTypeConfig;
pub use EzRaft;
pub use EzSnapshot;
pub use EzSnapshotMeta;
pub use EzStorage;
pub use FileStorage;
pub use Loaded;
pub use Persist;
pub use EzVote;
pub use OpenRaftTypes;