use anyhow::{Context, Result};
use openraft::{BasicNode, Config, Raft};
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::info;
use super::network::RaftHttpNetwork;
use super::raft_log::LogStore;
use super::raft_sm::{StateMachine, StateMachineData, TypeConfig};
use tatara_core::cluster::types::{ClusterCommand, ClusterResponse, NodeId};
pub type TataraRaft = Raft<TypeConfig>;
pub struct RaftCluster {
pub raft: TataraRaft,
pub state_machine: Arc<RwLock<StateMachineData>>,
pub network: RaftHttpNetwork,
node_id: NodeId,
}
impl RaftCluster {
pub async fn start(node_id: NodeId, _raft_addr: &str, data_dir: &PathBuf) -> Result<Self> {
let config = Config {
heartbeat_interval: 500,
election_timeout_min: 1500,
election_timeout_max: 3000,
max_in_snapshot_log_to_keep: 500,
..Default::default()
};
let config = Arc::new(config.validate().context("Invalid Raft config")?);
let log_path = data_dir.join("raft.redb");
let log_store = LogStore::new(&log_path).context("Failed to open Raft log store")?;
let state_machine = StateMachine::new();
let sm_data = state_machine.state();
let network = RaftHttpNetwork::new();
let raft = Raft::new(node_id, config, network.clone(), log_store, state_machine)
.await
.context("Failed to initialize Raft")?;
info!(node_id = node_id, "Raft node initialized");
Ok(Self {
raft,
state_machine: sm_data,
network,
node_id,
})
}
pub async fn bootstrap_single(&self, raft_addr: &str) -> Result<()> {
let mut members = BTreeMap::new();
members.insert(
self.node_id,
BasicNode {
addr: raft_addr.to_string(),
},
);
self.raft
.initialize(members)
.await
.map_err(|e| anyhow::anyhow!("Raft bootstrap failed: {}", e))?;
info!(
node_id = self.node_id,
"Bootstrapped single-node Raft cluster"
);
Ok(())
}
pub async fn add_voter(&self, node_id: NodeId, addr: &str) -> Result<()> {
let node = BasicNode {
addr: addr.to_string(),
};
self.raft
.add_learner(node_id, node, true)
.await
.map_err(|e| anyhow::anyhow!("Failed to add learner: {}", e))?;
let members = self.current_members().await?;
let mut new_members = members;
new_members.insert(node_id);
self.raft
.change_membership(new_members, false)
.await
.map_err(|e| anyhow::anyhow!("Failed to change membership: {}", e))?;
info!(
node_id = node_id,
addr = addr,
"Added voter to Raft cluster"
);
Ok(())
}
pub async fn add_learner(&self, node_id: NodeId, addr: &str) -> Result<()> {
let node = BasicNode {
addr: addr.to_string(),
};
self.raft
.add_learner(node_id, node, true)
.await
.map_err(|e| anyhow::anyhow!("Failed to add learner: {}", e))?;
info!(node_id = node_id, "Added learner to Raft cluster");
Ok(())
}
pub async fn write(&self, cmd: ClusterCommand) -> Result<ClusterResponse> {
let resp = self
.raft
.client_write(cmd)
.await
.map_err(|e| anyhow::anyhow!("Raft write failed: {}", e))?;
Ok(resp.data)
}
pub async fn read_state(&self) -> Result<Arc<RwLock<StateMachineData>>> {
self.raft
.ensure_linearizable()
.await
.map_err(|e| anyhow::anyhow!("Linearizable read failed: {}", e))?;
Ok(self.state_machine.clone())
}
pub async fn read_local(&self) -> Arc<RwLock<StateMachineData>> {
self.state_machine.clone()
}
pub fn read_local_sync(&self) -> Arc<RwLock<StateMachineData>> {
self.state_machine.clone()
}
pub async fn is_leader(&self) -> bool {
self.raft.ensure_linearizable().await.is_ok()
}
pub async fn current_leader(&self) -> Option<NodeId> {
self.raft.current_leader().await
}
async fn current_members(&self) -> Result<std::collections::BTreeSet<NodeId>> {
let metrics = self.raft.metrics().borrow().clone();
Ok(metrics.membership_config.membership().voter_ids().collect())
}
pub fn node_id(&self) -> NodeId {
self.node_id
}
}