init
This commit is contained in:
commit
2aa4be8248
15 changed files with 4891 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/target
|
3433
Cargo.lock
generated
Normal file
3433
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
44
Cargo.toml
Normal file
44
Cargo.toml
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
[package]
|
||||||
|
name = "dcache"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
openraft = { version = "0.8.3", features = ["serde"]}
|
||||||
|
#libmcaptcha = { branch = "master", git = "https://github.com/mCaptcha/libmcaptcha", features = ["full"]}
|
||||||
|
libmcaptcha = { path="../libmcaptcha/", features = ["full"]}
|
||||||
|
tracing = { version = "0.1.37", features = ["log"] }
|
||||||
|
serde_json = "1.0.96"
|
||||||
|
serde = { version = "1.0.163", features = ["derive"] }
|
||||||
|
byteorder = "1.4.3"
|
||||||
|
actix-web = "4"
|
||||||
|
actix-web-httpauth = "0.8.0"
|
||||||
|
futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
pretty_env_logger = "0.4.0"
|
||||||
|
uuid = { version = "1", features = ["v4"] }
|
||||||
|
sqlx = { version = "0.5.13", features = [ "runtime-actix-rustls", "postgres", "time", "offline" ] }
|
||||||
|
actix-web-codegen-const-routes = { version = "0.1.0", tag = "0.1.0", git = "https://github.com/realaravinth/actix-web-codegen-const-routes" }
|
||||||
|
derive_builder = "0.11.2"
|
||||||
|
config = "0.11"
|
||||||
|
derive_more = "0.99.17"
|
||||||
|
url = { version = "2.2.2", features = ["serde"]}
|
||||||
|
async-trait = "0.1.36"
|
||||||
|
clap = { version = "4.1.11", features = ["derive", "env"] }
|
||||||
|
reqwest = { version = "0.11.9", features = ["json"] }
|
||||||
|
tokio = { version = "1.0", default-features = false, features = ["sync"] }
|
||||||
|
tracing-subscriber = { version = "0.3.0", features = ["env-filter"] }
|
||||||
|
actix = "0.13.0"
|
||||||
|
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
serde_json = "1"
|
||||||
|
sqlx = { version = "0.5.13", features = [ "runtime-actix-rustls", "postgres", "time", "offline"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
actix-rt = "2.7.0"
|
||||||
|
base64 = "0.13.0"
|
||||||
|
anyhow = "1.0.63"
|
||||||
|
maplit = "1.0.2"
|
17
src/app.rs
Normal file
17
src/app.rs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use openraft::Config;
|
||||||
|
|
||||||
|
use crate::ExampleNodeId;
|
||||||
|
use crate::ExampleRaft;
|
||||||
|
use crate::ExampleStore;
|
||||||
|
|
||||||
|
// Representation of an application state. This struct can be shared around to share
|
||||||
|
// instances of raft, store and more.
|
||||||
|
pub struct ExampleApp {
|
||||||
|
pub id: ExampleNodeId,
|
||||||
|
pub addr: String,
|
||||||
|
pub raft: ExampleRaft,
|
||||||
|
pub store: Arc<ExampleStore>,
|
||||||
|
pub config: Arc<Config>,
|
||||||
|
}
|
36
src/bin/main.rs
Normal file
36
src/bin/main.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
use clap::Parser;
|
||||||
|
use dcache::network::raft_network_impl::ExampleNetwork;
|
||||||
|
use dcache::start_example_raft_node;
|
||||||
|
use dcache::store::ExampleStore;
|
||||||
|
use dcache::ExampleTypeConfig;
|
||||||
|
use openraft::Raft;
|
||||||
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
|
pub type ExampleRaft = Raft<ExampleTypeConfig, ExampleNetwork, ExampleStore>;
|
||||||
|
|
||||||
|
#[derive(Parser, Clone, Debug)]
|
||||||
|
#[clap(author, version, about, long_about = None)]
|
||||||
|
pub struct Opt {
|
||||||
|
#[clap(long)]
|
||||||
|
pub id: u64,
|
||||||
|
|
||||||
|
#[clap(long)]
|
||||||
|
pub http_addr: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
// Setup the logger
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_target(true)
|
||||||
|
.with_thread_ids(true)
|
||||||
|
.with_level(true)
|
||||||
|
.with_ansi(false)
|
||||||
|
.with_env_filter(EnvFilter::from_default_env())
|
||||||
|
.init();
|
||||||
|
|
||||||
|
// Parse the parameters passed by arguments.
|
||||||
|
let options = Opt::parse();
|
||||||
|
|
||||||
|
start_example_raft_node(options.id, options.http_addr).await
|
||||||
|
}
|
227
src/client.rs
Normal file
227
src/client.rs
Normal file
|
@ -0,0 +1,227 @@
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use openraft::error::ForwardToLeader;
|
||||||
|
use openraft::error::NetworkError;
|
||||||
|
use openraft::error::RPCError;
|
||||||
|
use openraft::error::RemoteError;
|
||||||
|
use openraft::BasicNode;
|
||||||
|
use openraft::RaftMetrics;
|
||||||
|
use openraft::TryAsRef;
|
||||||
|
use reqwest::Client;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
use tokio::time::timeout;
|
||||||
|
|
||||||
|
use crate::typ;
|
||||||
|
use crate::ExampleNodeId;
|
||||||
|
use crate::ExampleRequest;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Empty {}
|
||||||
|
|
||||||
|
pub struct ExampleClient {
|
||||||
|
/// The leader node to send request to.
|
||||||
|
///
|
||||||
|
/// All traffic should be sent to the leader in a cluster.
|
||||||
|
pub leader: Arc<Mutex<(ExampleNodeId, String)>>,
|
||||||
|
|
||||||
|
pub inner: Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExampleClient {
|
||||||
|
/// Create a client with a leader node id and a node manager to get node address by node id.
|
||||||
|
pub fn new(leader_id: ExampleNodeId, leader_addr: String) -> Self {
|
||||||
|
Self {
|
||||||
|
leader: Arc::new(Mutex::new((leader_id, leader_addr))),
|
||||||
|
inner: reqwest::Client::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Application API
|
||||||
|
|
||||||
|
/// Submit a write request to the raft cluster.
|
||||||
|
///
|
||||||
|
/// The request will be processed by raft protocol: it will be replicated to a quorum and then
|
||||||
|
/// will be applied to state machine.
|
||||||
|
///
|
||||||
|
/// The result of applying the request will be returned.
|
||||||
|
pub async fn write(
|
||||||
|
&self,
|
||||||
|
req: &ExampleRequest,
|
||||||
|
) -> Result<typ::ClientWriteResponse, typ::RPCError<typ::ClientWriteError>> {
|
||||||
|
self.send_rpc_to_leader("write", Some(req)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read value by key, in an inconsistent mode.
|
||||||
|
///
|
||||||
|
/// This method may return stale value because it does not force to read on a legal leader.
|
||||||
|
pub async fn read(&self, req: &String) -> Result<String, typ::RPCError> {
|
||||||
|
self.do_send_rpc_to_leader("read", Some(req)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consistent Read value by key, in an inconsistent mode.
|
||||||
|
///
|
||||||
|
/// This method MUST return consistent value or CheckIsLeaderError.
|
||||||
|
pub async fn consistent_read(
|
||||||
|
&self,
|
||||||
|
req: &String,
|
||||||
|
) -> Result<String, typ::RPCError<typ::CheckIsLeaderError>> {
|
||||||
|
self.do_send_rpc_to_leader("consistent_read", Some(req))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Cluster management API
|
||||||
|
|
||||||
|
/// Initialize a cluster of only the node that receives this request.
|
||||||
|
///
|
||||||
|
/// This is the first step to initialize a cluster.
|
||||||
|
/// With a initialized cluster, new node can be added with [`write`].
|
||||||
|
/// Then setup replication with [`add_learner`].
|
||||||
|
/// Then make the new node a member with [`change_membership`].
|
||||||
|
pub async fn init(&self) -> Result<(), typ::RPCError<typ::InitializeError>> {
|
||||||
|
self.do_send_rpc_to_leader("init", Some(&Empty {})).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a node as learner.
|
||||||
|
///
|
||||||
|
/// The node to add has to exist, i.e., being added with `write(ExampleRequest::AddNode{})`
|
||||||
|
pub async fn add_learner(
|
||||||
|
&self,
|
||||||
|
req: (ExampleNodeId, String),
|
||||||
|
) -> Result<typ::ClientWriteResponse, typ::RPCError<typ::ClientWriteError>> {
|
||||||
|
self.send_rpc_to_leader("add-learner", Some(&req)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change membership to the specified set of nodes.
|
||||||
|
///
|
||||||
|
/// All nodes in `req` have to be already added as learner with [`add_learner`],
|
||||||
|
/// or an error [`LearnerNotFound`] will be returned.
|
||||||
|
pub async fn change_membership(
|
||||||
|
&self,
|
||||||
|
req: &BTreeSet<ExampleNodeId>,
|
||||||
|
) -> Result<typ::ClientWriteResponse, typ::RPCError<typ::ClientWriteError>> {
|
||||||
|
self.send_rpc_to_leader("change-membership", Some(req))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the metrics about the cluster.
|
||||||
|
///
|
||||||
|
/// Metrics contains various information about the cluster, such as current leader,
|
||||||
|
/// membership config, replication status etc.
|
||||||
|
/// See [`RaftMetrics`].
|
||||||
|
pub async fn metrics(&self) -> Result<RaftMetrics<ExampleNodeId, BasicNode>, typ::RPCError> {
|
||||||
|
self.do_send_rpc_to_leader("metrics", None::<&()>).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Internal methods
|
||||||
|
|
||||||
|
/// Send RPC to specified node.
|
||||||
|
///
|
||||||
|
/// It sends out a POST request if `req` is Some. Otherwise a GET request.
|
||||||
|
/// The remote endpoint must respond a reply in form of `Result<T, E>`.
|
||||||
|
/// An `Err` happened on remote will be wrapped in an [`RPCError::RemoteError`].
|
||||||
|
async fn do_send_rpc_to_leader<Req, Resp, Err>(
|
||||||
|
&self,
|
||||||
|
uri: &str,
|
||||||
|
req: Option<&Req>,
|
||||||
|
) -> Result<Resp, typ::RPCError<Err>>
|
||||||
|
where
|
||||||
|
Req: Serialize + 'static,
|
||||||
|
Resp: Serialize + DeserializeOwned,
|
||||||
|
Err: std::error::Error + Serialize + DeserializeOwned,
|
||||||
|
{
|
||||||
|
let (leader_id, url) = {
|
||||||
|
let t = self.leader.lock().unwrap();
|
||||||
|
let target_addr = &t.1;
|
||||||
|
(t.0, format!("http://{}/{}", target_addr, uri))
|
||||||
|
};
|
||||||
|
|
||||||
|
let fu = if let Some(r) = req {
|
||||||
|
tracing::debug!(
|
||||||
|
">>> client send request to {}: {}",
|
||||||
|
url,
|
||||||
|
serde_json::to_string_pretty(&r).unwrap()
|
||||||
|
);
|
||||||
|
self.inner.post(url.clone()).json(r)
|
||||||
|
} else {
|
||||||
|
tracing::debug!(">>> client send request to {}", url,);
|
||||||
|
self.inner.get(url.clone())
|
||||||
|
}
|
||||||
|
.send();
|
||||||
|
|
||||||
|
let res = timeout(Duration::from_millis(3_000), fu).await;
|
||||||
|
let resp = match res {
|
||||||
|
Ok(x) => x.map_err(|e| RPCError::Network(NetworkError::new(&e)))?,
|
||||||
|
Err(timeout_err) => {
|
||||||
|
tracing::error!("timeout {} to url: {}", timeout_err, url);
|
||||||
|
return Err(RPCError::Network(NetworkError::new(&timeout_err)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let res: Result<Resp, typ::RaftError<Err>> = resp
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.map_err(|e| RPCError::Network(NetworkError::new(&e)))?;
|
||||||
|
tracing::debug!(
|
||||||
|
"<<< client recv reply from {}: {}",
|
||||||
|
url,
|
||||||
|
serde_json::to_string_pretty(&res).unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
res.map_err(|e| RPCError::RemoteError(RemoteError::new(leader_id, e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try the best to send a request to the leader.
|
||||||
|
///
|
||||||
|
/// If the target node is not a leader, a `ForwardToLeader` error will be
|
||||||
|
/// returned and this client will retry at most 3 times to contact the updated leader.
|
||||||
|
async fn send_rpc_to_leader<Req, Resp, Err>(
|
||||||
|
&self,
|
||||||
|
uri: &str,
|
||||||
|
req: Option<&Req>,
|
||||||
|
) -> Result<Resp, typ::RPCError<Err>>
|
||||||
|
where
|
||||||
|
Req: Serialize + 'static,
|
||||||
|
Resp: Serialize + DeserializeOwned,
|
||||||
|
Err: std::error::Error
|
||||||
|
+ Serialize
|
||||||
|
+ DeserializeOwned
|
||||||
|
+ TryAsRef<typ::ForwardToLeader>
|
||||||
|
+ Clone,
|
||||||
|
{
|
||||||
|
// Retry at most 3 times to find a valid leader.
|
||||||
|
let mut n_retry = 3;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let res: Result<Resp, typ::RPCError<Err>> = self.do_send_rpc_to_leader(uri, req).await;
|
||||||
|
|
||||||
|
let rpc_err = match res {
|
||||||
|
Ok(x) => return Ok(x),
|
||||||
|
Err(rpc_err) => rpc_err,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(ForwardToLeader {
|
||||||
|
leader_id: Some(leader_id),
|
||||||
|
leader_node: Some(leader_node),
|
||||||
|
}) = rpc_err.forward_to_leader()
|
||||||
|
{
|
||||||
|
// Update target to the new leader.
|
||||||
|
{
|
||||||
|
let mut t = self.leader.lock().unwrap();
|
||||||
|
*t = (*leader_id, leader_node.addr.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
n_retry -= 1;
|
||||||
|
if n_retry > 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(rpc_err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
121
src/lib.rs
Normal file
121
src/lib.rs
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
#![allow(clippy::uninlined_format_args)]
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use actix_web::middleware;
|
||||||
|
use actix_web::middleware::Logger;
|
||||||
|
use actix_web::web::Data;
|
||||||
|
use actix_web::App;
|
||||||
|
use actix_web::HttpServer;
|
||||||
|
use openraft::BasicNode;
|
||||||
|
use openraft::Config;
|
||||||
|
use openraft::Raft;
|
||||||
|
|
||||||
|
use crate::app::ExampleApp;
|
||||||
|
use crate::network::api;
|
||||||
|
use crate::network::management;
|
||||||
|
use crate::network::raft;
|
||||||
|
use crate::network::raft_network_impl::ExampleNetwork;
|
||||||
|
use crate::store::ExampleRequest;
|
||||||
|
use crate::store::ExampleResponse;
|
||||||
|
use crate::store::ExampleStore;
|
||||||
|
|
||||||
|
pub mod app;
|
||||||
|
pub mod client;
|
||||||
|
pub mod network;
|
||||||
|
pub mod store;
|
||||||
|
|
||||||
|
pub type ExampleNodeId = u64;
|
||||||
|
|
||||||
|
openraft::declare_raft_types!(
|
||||||
|
/// Declare the type configuration for example K/V store.
|
||||||
|
pub ExampleTypeConfig: D = ExampleRequest, R = ExampleResponse, NodeId = ExampleNodeId, Node = BasicNode
|
||||||
|
);
|
||||||
|
|
||||||
|
pub type ExampleRaft = Raft<ExampleTypeConfig, ExampleNetwork, Arc<ExampleStore>>;
|
||||||
|
|
||||||
|
pub mod typ {
|
||||||
|
use openraft::BasicNode;
|
||||||
|
|
||||||
|
use crate::ExampleNodeId;
|
||||||
|
use crate::ExampleTypeConfig;
|
||||||
|
|
||||||
|
pub type RaftError<E = openraft::error::Infallible> =
|
||||||
|
openraft::error::RaftError<ExampleNodeId, E>;
|
||||||
|
pub type RPCError<E = openraft::error::Infallible> =
|
||||||
|
openraft::error::RPCError<ExampleNodeId, BasicNode, RaftError<E>>;
|
||||||
|
|
||||||
|
pub type ClientWriteError = openraft::error::ClientWriteError<ExampleNodeId, BasicNode>;
|
||||||
|
pub type CheckIsLeaderError = openraft::error::CheckIsLeaderError<ExampleNodeId, BasicNode>;
|
||||||
|
pub type ForwardToLeader = openraft::error::ForwardToLeader<ExampleNodeId, BasicNode>;
|
||||||
|
pub type InitializeError = openraft::error::InitializeError<ExampleNodeId, BasicNode>;
|
||||||
|
|
||||||
|
pub type ClientWriteResponse = openraft::raft::ClientWriteResponse<ExampleTypeConfig>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start_example_raft_node(
|
||||||
|
node_id: ExampleNodeId,
|
||||||
|
http_addr: String,
|
||||||
|
) -> std::io::Result<()> {
|
||||||
|
// Create a configuration for the raft instance.
|
||||||
|
let config = Config {
|
||||||
|
heartbeat_interval: 500,
|
||||||
|
election_timeout_min: 1500,
|
||||||
|
election_timeout_max: 3000,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let salt = "foobar12".into();
|
||||||
|
#[cfg(release)]
|
||||||
|
todo!("set salt");
|
||||||
|
|
||||||
|
let config = Arc::new(config.validate().unwrap());
|
||||||
|
|
||||||
|
// Create a instance of where the Raft data will be stored.
|
||||||
|
let store = Arc::new(ExampleStore::new(salt));
|
||||||
|
|
||||||
|
// Create the network layer that will connect and communicate the raft instances and
|
||||||
|
// will be used in conjunction with the store created above.
|
||||||
|
let network = ExampleNetwork {};
|
||||||
|
|
||||||
|
// Create a local raft instance.
|
||||||
|
let raft = Raft::new(node_id, config.clone(), network, store.clone())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Create an application that will store all the instances created above, this will
|
||||||
|
// be later used on the actix-web services.
|
||||||
|
let app = Data::new(ExampleApp {
|
||||||
|
id: node_id,
|
||||||
|
addr: http_addr.clone(),
|
||||||
|
raft,
|
||||||
|
store,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start the actix-web server.
|
||||||
|
let server = HttpServer::new(move || {
|
||||||
|
App::new()
|
||||||
|
.wrap(Logger::default())
|
||||||
|
.wrap(Logger::new("%a %{User-Agent}i"))
|
||||||
|
.wrap(middleware::Compress::default())
|
||||||
|
.app_data(app.clone())
|
||||||
|
// raft internal RPC
|
||||||
|
.service(raft::append)
|
||||||
|
.service(raft::snapshot)
|
||||||
|
.service(raft::vote)
|
||||||
|
// admin API
|
||||||
|
.service(management::init)
|
||||||
|
.service(management::add_learner)
|
||||||
|
.service(management::change_membership)
|
||||||
|
.service(management::metrics)
|
||||||
|
// application API
|
||||||
|
.service(api::write)
|
||||||
|
// .service(api::read)
|
||||||
|
// .service(api::consistent_read)
|
||||||
|
});
|
||||||
|
|
||||||
|
let x = server.bind(http_addr)?;
|
||||||
|
|
||||||
|
x.run().await
|
||||||
|
}
|
54
src/network/api.rs
Normal file
54
src/network/api.rs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
use actix_web::post;
|
||||||
|
use actix_web::web;
|
||||||
|
use actix_web::web::Data;
|
||||||
|
use actix_web::Responder;
|
||||||
|
use openraft::error::CheckIsLeaderError;
|
||||||
|
use openraft::error::Infallible;
|
||||||
|
use openraft::error::RaftError;
|
||||||
|
use openraft::BasicNode;
|
||||||
|
use web::Json;
|
||||||
|
|
||||||
|
use crate::app::ExampleApp;
|
||||||
|
use crate::store::ExampleRequest;
|
||||||
|
use crate::ExampleNodeId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Application API
|
||||||
|
*
|
||||||
|
* This is where you place your application, you can use the example below to create your
|
||||||
|
* API. The current implementation:
|
||||||
|
*
|
||||||
|
* - `POST - /write` saves a value in a key and sync the nodes.
|
||||||
|
* - `POST - /read` attempt to find a value from a given key.
|
||||||
|
*/
|
||||||
|
#[post("/write")]
|
||||||
|
pub async fn write(
|
||||||
|
app: Data<ExampleApp>,
|
||||||
|
req: Json<ExampleRequest>,
|
||||||
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
let response = app.raft.client_write(req.0).await;
|
||||||
|
Ok(Json(response))
|
||||||
|
}
|
||||||
|
// AddVisitor(AddVisitor),
|
||||||
|
// AddCaptcha(AddCaptcha),
|
||||||
|
// RenameCaptcha(RenameCaptcha),
|
||||||
|
// RemoveCaptcha(RemoveCaptcha),
|
||||||
|
//#[post("/post")]
|
||||||
|
//pub async fn read(app: Data<ExampleApp>, req: Json<String>) -> actix_web::Result<impl Responder> {
|
||||||
|
// let state_machine = app.store.state_machine.read().await;
|
||||||
|
// let key = req.0;
|
||||||
|
// let value = state_machine.data.get(&key).cloned();
|
||||||
|
//
|
||||||
|
// let res: Result<String, Infallible> = Ok(value.unwrap_or_default());
|
||||||
|
// Ok(Json(res))
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//#[post("/visitor/add")]
|
||||||
|
//pub async fn add_visitor(app: Data<ExampleApp>, req: Json<String>) -> actix_web::Result<impl Responder> {
|
||||||
|
// let state_machine = app.store.state_machine.read().await;
|
||||||
|
// let key = req.0;
|
||||||
|
// let value = state_machine.data.get(&key).cloned();
|
||||||
|
//
|
||||||
|
// let res: Result<String, Infallible> = Ok(value.unwrap_or_default());
|
||||||
|
// Ok(Json(res))
|
||||||
|
//}
|
68
src/network/management.rs
Normal file
68
src/network/management.rs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
|
use actix_web::get;
|
||||||
|
use actix_web::post;
|
||||||
|
use actix_web::web;
|
||||||
|
use actix_web::web::Data;
|
||||||
|
use actix_web::Responder;
|
||||||
|
use openraft::error::Infallible;
|
||||||
|
use openraft::BasicNode;
|
||||||
|
use openraft::RaftMetrics;
|
||||||
|
use web::Json;
|
||||||
|
|
||||||
|
use crate::app::ExampleApp;
|
||||||
|
use crate::ExampleNodeId;
|
||||||
|
|
||||||
|
// --- Cluster management
|
||||||
|
|
||||||
|
/// Add a node as **Learner**.
|
||||||
|
///
|
||||||
|
/// A Learner receives log replication from the leader but does not vote.
|
||||||
|
/// This should be done before adding a node as a member into the cluster
|
||||||
|
/// (by calling `change-membership`)
|
||||||
|
#[post("/add-learner")]
|
||||||
|
pub async fn add_learner(
|
||||||
|
app: Data<ExampleApp>,
|
||||||
|
req: Json<(ExampleNodeId, String)>,
|
||||||
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
let node_id = req.0 .0;
|
||||||
|
let node = BasicNode {
|
||||||
|
addr: req.0 .1.clone(),
|
||||||
|
};
|
||||||
|
let res = app.raft.add_learner(node_id, node, true).await;
|
||||||
|
Ok(Json(res))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Changes specified learners to members, or remove members.
|
||||||
|
#[post("/change-membership")]
|
||||||
|
pub async fn change_membership(
|
||||||
|
app: Data<ExampleApp>,
|
||||||
|
req: Json<BTreeSet<ExampleNodeId>>,
|
||||||
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
let res = app.raft.change_membership(req.0, false).await;
|
||||||
|
Ok(Json(res))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize a single-node cluster.
|
||||||
|
#[post("/init")]
|
||||||
|
pub async fn init(app: Data<ExampleApp>) -> actix_web::Result<impl Responder> {
|
||||||
|
let mut nodes = BTreeMap::new();
|
||||||
|
nodes.insert(
|
||||||
|
app.id,
|
||||||
|
BasicNode {
|
||||||
|
addr: app.addr.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let res = app.raft.initialize(nodes).await;
|
||||||
|
Ok(Json(res))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the latest metrics of the cluster
|
||||||
|
#[get("/metrics")]
|
||||||
|
pub async fn metrics(app: Data<ExampleApp>) -> actix_web::Result<impl Responder> {
|
||||||
|
let metrics = app.raft.metrics().borrow().clone();
|
||||||
|
|
||||||
|
let res: Result<RaftMetrics<ExampleNodeId, BasicNode>, Infallible> = Ok(metrics);
|
||||||
|
Ok(Json(res))
|
||||||
|
}
|
4
src/network/mod.rs
Normal file
4
src/network/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod api;
|
||||||
|
pub mod management;
|
||||||
|
pub mod raft;
|
||||||
|
pub mod raft_network_impl;
|
41
src/network/raft.rs
Normal file
41
src/network/raft.rs
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
use actix_web::post;
|
||||||
|
use actix_web::web;
|
||||||
|
use actix_web::web::Data;
|
||||||
|
use actix_web::Responder;
|
||||||
|
use openraft::raft::AppendEntriesRequest;
|
||||||
|
use openraft::raft::InstallSnapshotRequest;
|
||||||
|
use openraft::raft::VoteRequest;
|
||||||
|
use web::Json;
|
||||||
|
|
||||||
|
use crate::app::ExampleApp;
|
||||||
|
use crate::ExampleNodeId;
|
||||||
|
use crate::ExampleTypeConfig;
|
||||||
|
|
||||||
|
// --- Raft communication
|
||||||
|
|
||||||
|
#[post("/raft-vote")]
|
||||||
|
pub async fn vote(
|
||||||
|
app: Data<ExampleApp>,
|
||||||
|
req: Json<VoteRequest<ExampleNodeId>>,
|
||||||
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
let res = app.raft.vote(req.0).await;
|
||||||
|
Ok(Json(res))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/raft-append")]
|
||||||
|
pub async fn append(
|
||||||
|
app: Data<ExampleApp>,
|
||||||
|
req: Json<AppendEntriesRequest<ExampleTypeConfig>>,
|
||||||
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
let res = app.raft.append_entries(req.0).await;
|
||||||
|
Ok(Json(res))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/raft-snapshot")]
|
||||||
|
pub async fn snapshot(
|
||||||
|
app: Data<ExampleApp>,
|
||||||
|
req: Json<InstallSnapshotRequest<ExampleTypeConfig>>,
|
||||||
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
let res = app.raft.install_snapshot(req.0).await;
|
||||||
|
Ok(Json(res))
|
||||||
|
}
|
123
src/network/raft_network_impl.rs
Normal file
123
src/network/raft_network_impl.rs
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use openraft::error::InstallSnapshotError;
|
||||||
|
use openraft::error::NetworkError;
|
||||||
|
use openraft::error::RPCError;
|
||||||
|
use openraft::error::RaftError;
|
||||||
|
use openraft::error::RemoteError;
|
||||||
|
use openraft::raft::AppendEntriesRequest;
|
||||||
|
use openraft::raft::AppendEntriesResponse;
|
||||||
|
use openraft::raft::InstallSnapshotRequest;
|
||||||
|
use openraft::raft::InstallSnapshotResponse;
|
||||||
|
use openraft::raft::VoteRequest;
|
||||||
|
use openraft::raft::VoteResponse;
|
||||||
|
use openraft::BasicNode;
|
||||||
|
use openraft::RaftNetwork;
|
||||||
|
use openraft::RaftNetworkFactory;
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::ExampleNodeId;
|
||||||
|
use crate::ExampleTypeConfig;
|
||||||
|
|
||||||
|
pub struct ExampleNetwork {}
|
||||||
|
|
||||||
|
impl ExampleNetwork {
|
||||||
|
pub async fn send_rpc<Req, Resp, Err>(
|
||||||
|
&self,
|
||||||
|
target: ExampleNodeId,
|
||||||
|
target_node: &BasicNode,
|
||||||
|
uri: &str,
|
||||||
|
req: Req,
|
||||||
|
) -> Result<Resp, RPCError<ExampleNodeId, BasicNode, Err>>
|
||||||
|
where
|
||||||
|
Req: Serialize,
|
||||||
|
Err: std::error::Error + DeserializeOwned,
|
||||||
|
Resp: DeserializeOwned,
|
||||||
|
{
|
||||||
|
let addr = &target_node.addr;
|
||||||
|
|
||||||
|
let url = format!("http://{}/{}", addr, uri);
|
||||||
|
|
||||||
|
tracing::debug!("send_rpc to url: {}", url);
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
tracing::debug!("client is created for: {}", url);
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.post(url)
|
||||||
|
.json(&req)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| RPCError::Network(NetworkError::new(&e)))?;
|
||||||
|
|
||||||
|
tracing::debug!("client.post() is sent");
|
||||||
|
|
||||||
|
let res: Result<Resp, Err> = resp
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.map_err(|e| RPCError::Network(NetworkError::new(&e)))?;
|
||||||
|
|
||||||
|
res.map_err(|e| RPCError::RemoteError(RemoteError::new(target, e)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: This could be implemented also on `Arc<ExampleNetwork>`, but since it's empty, implemented
|
||||||
|
// directly.
|
||||||
|
#[async_trait]
|
||||||
|
impl RaftNetworkFactory<ExampleTypeConfig> for ExampleNetwork {
|
||||||
|
type Network = ExampleNetworkConnection;
|
||||||
|
|
||||||
|
async fn new_client(&mut self, target: ExampleNodeId, node: &BasicNode) -> Self::Network {
|
||||||
|
ExampleNetworkConnection {
|
||||||
|
owner: ExampleNetwork {},
|
||||||
|
target,
|
||||||
|
target_node: node.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ExampleNetworkConnection {
|
||||||
|
owner: ExampleNetwork,
|
||||||
|
target: ExampleNodeId,
|
||||||
|
target_node: BasicNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl RaftNetwork<ExampleTypeConfig> for ExampleNetworkConnection {
|
||||||
|
async fn send_append_entries(
|
||||||
|
&mut self,
|
||||||
|
req: AppendEntriesRequest<ExampleTypeConfig>,
|
||||||
|
) -> Result<
|
||||||
|
AppendEntriesResponse<ExampleNodeId>,
|
||||||
|
RPCError<ExampleNodeId, BasicNode, RaftError<ExampleNodeId>>,
|
||||||
|
> {
|
||||||
|
self.owner
|
||||||
|
.send_rpc(self.target, &self.target_node, "raft-append", req)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_install_snapshot(
|
||||||
|
&mut self,
|
||||||
|
req: InstallSnapshotRequest<ExampleTypeConfig>,
|
||||||
|
) -> Result<
|
||||||
|
InstallSnapshotResponse<ExampleNodeId>,
|
||||||
|
RPCError<ExampleNodeId, BasicNode, RaftError<ExampleNodeId, InstallSnapshotError>>,
|
||||||
|
> {
|
||||||
|
self.owner
|
||||||
|
.send_rpc(self.target, &self.target_node, "raft-snapshot", req)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_vote(
|
||||||
|
&mut self,
|
||||||
|
req: VoteRequest<ExampleNodeId>,
|
||||||
|
) -> Result<
|
||||||
|
VoteResponse<ExampleNodeId>,
|
||||||
|
RPCError<ExampleNodeId, BasicNode, RaftError<ExampleNodeId>>,
|
||||||
|
> {
|
||||||
|
self.owner
|
||||||
|
.send_rpc(self.target, &self.target_node, "raft-vote", req)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
528
src/store/mod.rs
Normal file
528
src/store/mod.rs
Normal file
|
@ -0,0 +1,528 @@
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::io::Cursor;
|
||||||
|
use std::ops::RangeBounds;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use libmcaptcha::AddVisitorResult;
|
||||||
|
use libmcaptcha::MCaptcha;
|
||||||
|
use openraft::async_trait::async_trait;
|
||||||
|
use openraft::storage::LogState;
|
||||||
|
use openraft::storage::Snapshot;
|
||||||
|
use openraft::AnyError;
|
||||||
|
use openraft::BasicNode;
|
||||||
|
use openraft::Entry;
|
||||||
|
use openraft::EntryPayload;
|
||||||
|
use openraft::ErrorSubject;
|
||||||
|
use openraft::ErrorVerb;
|
||||||
|
use openraft::LogId;
|
||||||
|
use openraft::RaftLogReader;
|
||||||
|
use openraft::RaftSnapshotBuilder;
|
||||||
|
use openraft::RaftStorage;
|
||||||
|
use openraft::SnapshotMeta;
|
||||||
|
use openraft::StorageError;
|
||||||
|
use openraft::StorageIOError;
|
||||||
|
use openraft::StoredMembership;
|
||||||
|
use openraft::Vote;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
use sqlx::Statement;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
use url::quirks::set_pathname;
|
||||||
|
|
||||||
|
use crate::ExampleNodeId;
|
||||||
|
use crate::ExampleTypeConfig;
|
||||||
|
|
||||||
|
use actix::prelude::*;
|
||||||
|
use libmcaptcha::master::messages::{
|
||||||
|
AddSite as AddCaptcha, AddVisitor, GetInternalData, RemoveCaptcha, Rename as RenameCaptcha,
|
||||||
|
SetInternalData,
|
||||||
|
};
|
||||||
|
use libmcaptcha::{master::embedded::master::Master as EmbeddedMaster, system::System, HashCache};
|
||||||
|
|
||||||
|
pub mod system;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Here you will set the types of request that will interact with the raft nodes.
|
||||||
|
* For example the `Set` will be used to write data (key and value) to the raft database.
|
||||||
|
* The `AddNode` will append a new node to the current existing shared list of nodes.
|
||||||
|
* You will want to add any request that can write data in all nodes here.
|
||||||
|
*/
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub enum ExampleRequest {
|
||||||
|
//Set { key: String, value: String },
|
||||||
|
AddVisitor(AddVisitor),
|
||||||
|
AddCaptcha(AddCaptcha),
|
||||||
|
RenameCaptcha(RenameCaptcha),
|
||||||
|
RemoveCaptcha(RemoveCaptcha),
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Here you will defined what type of answer you expect from reading the data of a node.
|
||||||
|
* In this example it will return a optional value from a given key in
|
||||||
|
* the `ExampleRequest.Set`.
|
||||||
|
*
|
||||||
|
* TODO: Should we explain how to create multiple `AppDataResponse`?
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub enum ExampleResponse {
|
||||||
|
AddVisitorResult(Option<AddVisitorResult>),
|
||||||
|
Empty,
|
||||||
|
// AddCaptchaResult, All returns ()
|
||||||
|
// RenameCaptchaResult,
|
||||||
|
// RemoveCaptchaResult,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ExampleSnapshot {
|
||||||
|
pub meta: SnapshotMeta<ExampleNodeId, BasicNode>,
|
||||||
|
|
||||||
|
/// The data of the state machine at the time of this snapshot.
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Here defines a state machine of the raft, this state represents a copy of the data
|
||||||
|
* between each node. Note that we are using `serde` to serialize the `data`, which has
|
||||||
|
* a implementation to be serialized. Note that for this test we set both the key and
|
||||||
|
* value as String, but you could set any type of value that has the serialization impl.
|
||||||
|
*/
|
||||||
|
pub struct ExampleStateMachine {
|
||||||
|
pub last_applied_log: Option<LogId<ExampleNodeId>>,
|
||||||
|
|
||||||
|
pub last_membership: StoredMembership<ExampleNodeId, BasicNode>,
|
||||||
|
|
||||||
|
/// Application data.
|
||||||
|
pub data: Arc<System<HashCache, EmbeddedMaster>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
struct PersistableStateMachine {
|
||||||
|
last_applied_log: Option<LogId<ExampleNodeId>>,
|
||||||
|
|
||||||
|
last_membership: StoredMembership<ExampleNodeId, BasicNode>,
|
||||||
|
|
||||||
|
/// Application data.
|
||||||
|
data: HashMap<String, MCaptcha>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PersistableStateMachine {
|
||||||
|
async fn from_statemachine(m: &ExampleStateMachine) -> Self {
|
||||||
|
let internal_data = m
|
||||||
|
.data
|
||||||
|
.master
|
||||||
|
.send(GetInternalData)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
Self {
|
||||||
|
last_applied_log: m.last_applied_log.clone(),
|
||||||
|
last_membership: m.last_membership.clone(),
|
||||||
|
data: internal_data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn to_statemachine(
|
||||||
|
self,
|
||||||
|
data: Arc<System<HashCache, EmbeddedMaster>>,
|
||||||
|
) -> ExampleStateMachine {
|
||||||
|
data.master
|
||||||
|
.send(SetInternalData {
|
||||||
|
mcaptcha: self.data,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
ExampleStateMachine {
|
||||||
|
last_applied_log: self.last_applied_log,
|
||||||
|
last_membership: self.last_membership,
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ExampleStore {
|
||||||
|
last_purged_log_id: RwLock<Option<LogId<ExampleNodeId>>>,
|
||||||
|
|
||||||
|
/// The Raft log.
|
||||||
|
log: RwLock<BTreeMap<u64, Entry<ExampleTypeConfig>>>,
|
||||||
|
|
||||||
|
/// The Raft state machine.
|
||||||
|
pub state_machine: RwLock<ExampleStateMachine>,
|
||||||
|
|
||||||
|
/// The current granted vote.
|
||||||
|
vote: RwLock<Option<Vote<ExampleNodeId>>>,
|
||||||
|
|
||||||
|
snapshot_idx: Arc<Mutex<u64>>,
|
||||||
|
|
||||||
|
current_snapshot: RwLock<Option<ExampleSnapshot>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExampleStore {
|
||||||
|
pub fn new(salt: String) -> Self {
|
||||||
|
let state_machine = RwLock::new(ExampleStateMachine {
|
||||||
|
last_applied_log: Default::default(),
|
||||||
|
last_membership: Default::default(),
|
||||||
|
data: system::init_system(salt),
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
last_purged_log_id: Default::default(),
|
||||||
|
log: Default::default(),
|
||||||
|
state_machine,
|
||||||
|
vote: Default::default(),
|
||||||
|
snapshot_idx: Default::default(),
|
||||||
|
current_snapshot: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl RaftLogReader<ExampleTypeConfig> for Arc<ExampleStore> {
|
||||||
|
async fn get_log_state(
|
||||||
|
&mut self,
|
||||||
|
) -> Result<LogState<ExampleTypeConfig>, StorageError<ExampleNodeId>> {
|
||||||
|
let log = self.log.read().await;
|
||||||
|
let last = log.iter().rev().next().map(|(_, ent)| ent.log_id);
|
||||||
|
|
||||||
|
let last_purged = *self.last_purged_log_id.read().await;
|
||||||
|
|
||||||
|
let last = match last {
|
||||||
|
None => last_purged,
|
||||||
|
Some(x) => Some(x),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(LogState {
|
||||||
|
last_purged_log_id: last_purged,
|
||||||
|
last_log_id: last,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn try_get_log_entries<RB: RangeBounds<u64> + Clone + Debug + Send + Sync>(
|
||||||
|
&mut self,
|
||||||
|
range: RB,
|
||||||
|
) -> Result<Vec<Entry<ExampleTypeConfig>>, StorageError<ExampleNodeId>> {
|
||||||
|
let log = self.log.read().await;
|
||||||
|
let response = log
|
||||||
|
.range(range.clone())
|
||||||
|
.map(|(_, val)| val.clone())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl RaftSnapshotBuilder<ExampleTypeConfig, Cursor<Vec<u8>>> for Arc<ExampleStore> {
|
||||||
|
#[tracing::instrument(level = "trace", skip(self))]
|
||||||
|
async fn build_snapshot(
|
||||||
|
&mut self,
|
||||||
|
) -> Result<Snapshot<ExampleNodeId, BasicNode, Cursor<Vec<u8>>>, StorageError<ExampleNodeId>>
|
||||||
|
{
|
||||||
|
let data;
|
||||||
|
let last_applied_log;
|
||||||
|
let last_membership;
|
||||||
|
|
||||||
|
{
|
||||||
|
// Serialize the data of the state machine.
|
||||||
|
let state_machine = self.state_machine.read().await;
|
||||||
|
let persistable_state_machine =
|
||||||
|
PersistableStateMachine::from_statemachine(&state_machine).await;
|
||||||
|
|
||||||
|
data = serde_json::to_vec(&persistable_state_machine).map_err(|e| {
|
||||||
|
StorageIOError::new(
|
||||||
|
ErrorSubject::StateMachine,
|
||||||
|
ErrorVerb::Read,
|
||||||
|
AnyError::new(&e),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
last_applied_log = state_machine.last_applied_log;
|
||||||
|
last_membership = state_machine.last_membership.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
let snapshot_idx = {
|
||||||
|
let mut l = self.snapshot_idx.lock().unwrap();
|
||||||
|
*l += 1;
|
||||||
|
*l
|
||||||
|
};
|
||||||
|
|
||||||
|
let snapshot_id = if let Some(last) = last_applied_log {
|
||||||
|
format!("{}-{}-{}", last.leader_id, last.index, snapshot_idx)
|
||||||
|
} else {
|
||||||
|
format!("--{}", snapshot_idx)
|
||||||
|
};
|
||||||
|
|
||||||
|
let meta = SnapshotMeta {
|
||||||
|
last_log_id: last_applied_log,
|
||||||
|
last_membership,
|
||||||
|
snapshot_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
let snapshot = ExampleSnapshot {
|
||||||
|
meta: meta.clone(),
|
||||||
|
data: data.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut current_snapshot = self.current_snapshot.write().await;
|
||||||
|
*current_snapshot = Some(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Snapshot {
|
||||||
|
meta,
|
||||||
|
snapshot: Box::new(Cursor::new(data)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl RaftStorage<ExampleTypeConfig> for Arc<ExampleStore> {
|
||||||
|
type SnapshotData = Cursor<Vec<u8>>;
|
||||||
|
type LogReader = Self;
|
||||||
|
type SnapshotBuilder = Self;
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "trace", skip(self))]
|
||||||
|
async fn save_vote(
|
||||||
|
&mut self,
|
||||||
|
vote: &Vote<ExampleNodeId>,
|
||||||
|
) -> Result<(), StorageError<ExampleNodeId>> {
|
||||||
|
let mut v = self.vote.write().await;
|
||||||
|
*v = Some(*vote);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_vote(
|
||||||
|
&mut self,
|
||||||
|
) -> Result<Option<Vote<ExampleNodeId>>, StorageError<ExampleNodeId>> {
|
||||||
|
Ok(*self.vote.read().await)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "trace", skip(self, entries))]
|
||||||
|
async fn append_to_log(
|
||||||
|
&mut self,
|
||||||
|
entries: &[&Entry<ExampleTypeConfig>],
|
||||||
|
) -> Result<(), StorageError<ExampleNodeId>> {
|
||||||
|
let mut log = self.log.write().await;
|
||||||
|
for entry in entries {
|
||||||
|
log.insert(entry.log_id.index, (*entry).clone());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "debug", skip(self))]
|
||||||
|
async fn delete_conflict_logs_since(
|
||||||
|
&mut self,
|
||||||
|
log_id: LogId<ExampleNodeId>,
|
||||||
|
) -> Result<(), StorageError<ExampleNodeId>> {
|
||||||
|
tracing::debug!("delete_log: [{:?}, +oo)", log_id);
|
||||||
|
|
||||||
|
let mut log = self.log.write().await;
|
||||||
|
let keys = log
|
||||||
|
.range(log_id.index..)
|
||||||
|
.map(|(k, _v)| *k)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for key in keys {
|
||||||
|
log.remove(&key);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "debug", skip(self))]
|
||||||
|
async fn purge_logs_upto(
|
||||||
|
&mut self,
|
||||||
|
log_id: LogId<ExampleNodeId>,
|
||||||
|
) -> Result<(), StorageError<ExampleNodeId>> {
|
||||||
|
tracing::debug!("delete_log: [{:?}, +oo)", log_id);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut ld = self.last_purged_log_id.write().await;
|
||||||
|
assert!(*ld <= Some(log_id));
|
||||||
|
*ld = Some(log_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut log = self.log.write().await;
|
||||||
|
|
||||||
|
let keys = log
|
||||||
|
.range(..=log_id.index)
|
||||||
|
.map(|(k, _v)| *k)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for key in keys {
|
||||||
|
log.remove(&key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn last_applied_state(
|
||||||
|
&mut self,
|
||||||
|
) -> Result<
|
||||||
|
(
|
||||||
|
Option<LogId<ExampleNodeId>>,
|
||||||
|
StoredMembership<ExampleNodeId, BasicNode>,
|
||||||
|
),
|
||||||
|
StorageError<ExampleNodeId>,
|
||||||
|
> {
|
||||||
|
let state_machine = self.state_machine.read().await;
|
||||||
|
Ok((
|
||||||
|
state_machine.last_applied_log,
|
||||||
|
state_machine.last_membership.clone(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "trace", skip(self, entries))]
|
||||||
|
async fn apply_to_state_machine(
|
||||||
|
&mut self,
|
||||||
|
entries: &[&Entry<ExampleTypeConfig>],
|
||||||
|
) -> Result<Vec<ExampleResponse>, StorageError<ExampleNodeId>> {
|
||||||
|
let mut res = Vec::with_capacity(entries.len());
|
||||||
|
|
||||||
|
let mut sm = self.state_machine.write().await;
|
||||||
|
|
||||||
|
for entry in entries {
|
||||||
|
tracing::debug!(%entry.log_id, "replicate to sm");
|
||||||
|
|
||||||
|
sm.last_applied_log = Some(entry.log_id);
|
||||||
|
|
||||||
|
match entry.payload {
|
||||||
|
EntryPayload::Blank => res.push(ExampleResponse::Empty),
|
||||||
|
EntryPayload::Normal(ref req) => match req {
|
||||||
|
ExampleRequest::AddVisitor(msg) => {
|
||||||
|
let sm = self.state_machine.read().await;
|
||||||
|
res.push(ExampleResponse::AddVisitorResult(
|
||||||
|
sm.data
|
||||||
|
.master
|
||||||
|
.send(msg.clone())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
ExampleRequest::AddCaptcha(msg) => {
|
||||||
|
let sm = self.state_machine.read().await;
|
||||||
|
sm.data
|
||||||
|
.master
|
||||||
|
.send(msg.clone())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
res.push(ExampleResponse::Empty);
|
||||||
|
}
|
||||||
|
ExampleRequest::RenameCaptcha(msg) => {
|
||||||
|
let sm = self.state_machine.read().await;
|
||||||
|
sm.data
|
||||||
|
.master
|
||||||
|
.send(msg.clone())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
res.push(ExampleResponse::Empty);
|
||||||
|
}
|
||||||
|
ExampleRequest::RemoveCaptcha(msg) => {
|
||||||
|
let sm = self.state_machine.read().await;
|
||||||
|
sm.data
|
||||||
|
.master
|
||||||
|
.send(msg.clone())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
res.push(ExampleResponse::Empty);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
EntryPayload::Membership(ref mem) => {
|
||||||
|
sm.last_membership = StoredMembership::new(Some(entry.log_id), mem.clone());
|
||||||
|
res.push(ExampleResponse::Empty)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "trace", skip(self))]
|
||||||
|
async fn begin_receiving_snapshot(
|
||||||
|
&mut self,
|
||||||
|
) -> Result<Box<Self::SnapshotData>, StorageError<ExampleNodeId>> {
|
||||||
|
Ok(Box::new(Cursor::new(Vec::new())))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "trace", skip(self, snapshot))]
|
||||||
|
async fn install_snapshot(
|
||||||
|
&mut self,
|
||||||
|
meta: &SnapshotMeta<ExampleNodeId, BasicNode>,
|
||||||
|
snapshot: Box<Self::SnapshotData>,
|
||||||
|
) -> Result<(), StorageError<ExampleNodeId>> {
|
||||||
|
tracing::info!(
|
||||||
|
{ snapshot_size = snapshot.get_ref().len() },
|
||||||
|
"decoding snapshot for installation"
|
||||||
|
);
|
||||||
|
|
||||||
|
let new_snapshot = ExampleSnapshot {
|
||||||
|
meta: meta.clone(),
|
||||||
|
data: snapshot.into_inner(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the state machine.
|
||||||
|
{
|
||||||
|
let updated_persistable_state_machine: PersistableStateMachine =
|
||||||
|
serde_json::from_slice(&new_snapshot.data).map_err(|e| {
|
||||||
|
StorageIOError::new(
|
||||||
|
ErrorSubject::Snapshot(new_snapshot.meta.signature()),
|
||||||
|
ErrorVerb::Read,
|
||||||
|
AnyError::new(&e),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut state_machine = self.state_machine.write().await;
|
||||||
|
let updated_state_machine = updated_persistable_state_machine
|
||||||
|
.to_statemachine(state_machine.data.clone())
|
||||||
|
.await;
|
||||||
|
*state_machine = updated_state_machine;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update current snapshot.
|
||||||
|
let mut current_snapshot = self.current_snapshot.write().await;
|
||||||
|
*current_snapshot = Some(new_snapshot);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "trace", skip(self))]
|
||||||
|
async fn get_current_snapshot(
|
||||||
|
&mut self,
|
||||||
|
) -> Result<
|
||||||
|
Option<Snapshot<ExampleNodeId, BasicNode, Self::SnapshotData>>,
|
||||||
|
StorageError<ExampleNodeId>,
|
||||||
|
> {
|
||||||
|
match &*self.current_snapshot.read().await {
|
||||||
|
Some(snapshot) => {
|
||||||
|
let data = snapshot.data.clone();
|
||||||
|
Ok(Some(Snapshot {
|
||||||
|
meta: snapshot.meta.clone(),
|
||||||
|
snapshot: Box::new(Cursor::new(data)),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_log_reader(&mut self) -> Self::LogReader {
|
||||||
|
self.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_snapshot_builder(&mut self) -> Self::SnapshotBuilder {
|
||||||
|
self.clone()
|
||||||
|
}
|
||||||
|
}
|
28
src/store/system.rs
Normal file
28
src/store/system.rs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use actix::prelude::*;
|
||||||
|
use libmcaptcha::{
|
||||||
|
cache::{hashcache::HashCache, messages::VerifyCaptchaResult},
|
||||||
|
master::embedded::master::Master,
|
||||||
|
master::messages::AddSiteBuilder,
|
||||||
|
pow::{ConfigBuilder, Work},
|
||||||
|
system::{System, SystemBuilder},
|
||||||
|
DefenseBuilder, LevelBuilder, MCaptchaBuilder,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init_system(salt: String) -> Arc<System<HashCache, Master>> {
|
||||||
|
let cache = HashCache::default().start();
|
||||||
|
|
||||||
|
let pow = ConfigBuilder::default().salt(salt).build().unwrap();
|
||||||
|
|
||||||
|
let master = Master::new(5).start();
|
||||||
|
|
||||||
|
let system = SystemBuilder::default()
|
||||||
|
.master(master)
|
||||||
|
.cache(cache)
|
||||||
|
.pow(pow.clone())
|
||||||
|
.runners(4)
|
||||||
|
.queue_length(2000)
|
||||||
|
.build();
|
||||||
|
Arc::new(system)
|
||||||
|
}
|
166
test-cluster.sh
Executable file
166
test-cluster.sh
Executable file
|
@ -0,0 +1,166 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -o errexit
|
||||||
|
|
||||||
|
cargo build
|
||||||
|
|
||||||
|
kill() {
|
||||||
|
if [ "$(uname)" = "Darwin" ]; then
|
||||||
|
SERVICE='raft-key-value'
|
||||||
|
if pgrep -xq -- "${SERVICE}"; then
|
||||||
|
pkill -f "${SERVICE}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
set +e # killall will error if finds no process to kill
|
||||||
|
killall raft-key-value
|
||||||
|
set -e
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
rpc() {
|
||||||
|
local uri=$1
|
||||||
|
local body="$2"
|
||||||
|
|
||||||
|
echo '---'" rpc(:$uri, $body)"
|
||||||
|
|
||||||
|
{
|
||||||
|
if [ ".$body" = "." ]; then
|
||||||
|
time curl --silent "127.0.0.1:$uri"
|
||||||
|
else
|
||||||
|
time curl --silent "127.0.0.1:$uri" -H "Content-Type: application/json" -d "$body"
|
||||||
|
fi
|
||||||
|
} | {
|
||||||
|
if type jq > /dev/null 2>&1; then
|
||||||
|
jq
|
||||||
|
else
|
||||||
|
cat
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
export RUST_LOG=trace
|
||||||
|
|
||||||
|
echo "Killing all running raft-key-value"
|
||||||
|
|
||||||
|
kill
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
echo "Start 3 uninitialized raft-key-value servers..."
|
||||||
|
|
||||||
|
nohup ./target/debug/raft-key-value --id 1 --http-addr 127.0.0.1:21001 > n1.log &
|
||||||
|
sleep 1
|
||||||
|
echo "Server 1 started"
|
||||||
|
|
||||||
|
nohup ./target/debug/raft-key-value --id 2 --http-addr 127.0.0.1:21002 > n2.log &
|
||||||
|
sleep 1
|
||||||
|
echo "Server 2 started"
|
||||||
|
|
||||||
|
nohup ./target/debug/raft-key-value --id 3 --http-addr 127.0.0.1:21003 > n3.log &
|
||||||
|
sleep 1
|
||||||
|
echo "Server 3 started"
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
echo "Initialize server 1 as a single-node cluster"
|
||||||
|
sleep 2
|
||||||
|
echo
|
||||||
|
rpc 21001/init '{}'
|
||||||
|
|
||||||
|
echo "Server 1 is a leader now"
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
echo "Get metrics from the leader"
|
||||||
|
sleep 2
|
||||||
|
echo
|
||||||
|
rpc 21001/metrics
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
|
||||||
|
echo "Adding node 2 and node 3 as learners, to receive log from leader node 1"
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
echo
|
||||||
|
rpc 21001/add-learner '[2, "127.0.0.1:21002"]'
|
||||||
|
echo "Node 2 added as learner"
|
||||||
|
sleep 1
|
||||||
|
echo
|
||||||
|
rpc 21001/add-learner '[3, "127.0.0.1:21003"]'
|
||||||
|
echo "Node 3 added as learner"
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
echo "Get metrics from the leader, after adding 2 learners"
|
||||||
|
sleep 2
|
||||||
|
echo
|
||||||
|
rpc 21001/metrics
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
echo "Changing membership from [1] to 3 nodes cluster: [1, 2, 3]"
|
||||||
|
echo
|
||||||
|
rpc 21001/change-membership '[1, 2, 3]'
|
||||||
|
sleep 1
|
||||||
|
echo 'Membership changed to [1, 2, 3]'
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
echo "Get metrics from the leader again"
|
||||||
|
sleep 1
|
||||||
|
echo
|
||||||
|
rpc 21001/metrics
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
echo "Write data on leader"
|
||||||
|
sleep 1
|
||||||
|
echo
|
||||||
|
rpc 21001/write '{"Set":{"key":"foo","value":"bar"}}'
|
||||||
|
sleep 1
|
||||||
|
echo "Data written"
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
echo "Read on every node, including the leader"
|
||||||
|
sleep 1
|
||||||
|
echo "Read from node 1"
|
||||||
|
echo
|
||||||
|
rpc 21001/read '"foo"'
|
||||||
|
echo "Read from node 2"
|
||||||
|
echo
|
||||||
|
rpc 21002/read '"foo"'
|
||||||
|
echo "Read from node 3"
|
||||||
|
echo
|
||||||
|
rpc 21003/read '"foo"'
|
||||||
|
|
||||||
|
|
||||||
|
echo "Changing membership from [1,2,3] to [3]"
|
||||||
|
echo
|
||||||
|
rpc 21001/change-membership '[3]'
|
||||||
|
sleep 1
|
||||||
|
echo 'Membership changed to [3]'
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
echo "Get metrics from the node-3"
|
||||||
|
sleep 1
|
||||||
|
echo
|
||||||
|
rpc 21003/metrics
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
|
||||||
|
echo "Write foo=zoo on node-3"
|
||||||
|
sleep 1
|
||||||
|
echo
|
||||||
|
rpc 21003/write '{"Set":{"key":"foo","value":"zoo"}}'
|
||||||
|
sleep 1
|
||||||
|
echo "Data written"
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
echo "Read foo=zoo from node-3"
|
||||||
|
sleep 1
|
||||||
|
echo "Read from node 3"
|
||||||
|
echo
|
||||||
|
rpc 21003/read '"foo"'
|
||||||
|
echo
|
||||||
|
|
||||||
|
|
||||||
|
echo "Killing all nodes..."
|
||||||
|
kill
|
Loading…
Reference in a new issue