use std::collections::BTreeSet;
use async_trait::async_trait;
use openraft::Raft;
use openraft::async_runtime::watch::WatchReceiver;
use openraft::error::{ChangeMembershipError, ClientWriteError, ForwardToLeader, RaftError};
use tokio::sync::Mutex;
use tsoracle_driver_openraft::{HighWaterStateMachine, OpenraftPeer, TypeConfig};
use crate::admin::{
AdminError, MemberEntry, MemberRole, MembershipAdmin, MembershipView, NewMember,
};
fn map_write_error(err: RaftError<TypeConfig, ClientWriteError<TypeConfig>>) -> AdminError {
match err {
RaftError::APIError(ClientWriteError::ForwardToLeader(ForwardToLeader {
leader_node,
..
})) => AdminError::NotLeader {
leader_admin_endpoint: leader_node
.map(|node| node.admin_endpoint)
.filter(|endpoint| !endpoint.is_empty()),
},
RaftError::APIError(ClientWriteError::ChangeMembershipError(
ChangeMembershipError::LearnerNotFound(learner),
)) => AdminError::NotMember(learner.node_id),
other => AdminError::Driver(other.to_string()),
}
}
fn map_activation_error(err: tsoracle_driver_openraft::FormatActivationError) -> AdminError {
use tsoracle_driver_openraft::FormatActivationError as FAE;
match err {
FAE::NotLeader => AdminError::NotLeader {
leader_admin_endpoint: None,
},
FAE::TargetOutOfRange { target, min, max } => {
AdminError::TargetOutOfRange { target, min, max }
}
FAE::MembersBelowTarget { target, incapable } => {
AdminError::MembersBelowTarget { target, incapable }
}
FAE::MemberUnreachable { node_id, detail } => AdminError::Driver(format!(
"format activation gate: member {node_id} unreachable: {detail}",
)),
FAE::MembershipChangedSinceGate { target } => {
AdminError::MembershipChangedSinceGate { target }
}
FAE::ProposalFailed(detail) => {
AdminError::Driver(format!("format activation proposal failed: {detail}",))
}
}
}
fn voters_with(current: &BTreeSet<u64>, id: u64) -> BTreeSet<u64> {
let mut next = current.clone();
next.insert(id);
next
}
fn voters_without(current: &BTreeSet<u64>, id: u64) -> BTreeSet<u64> {
let mut next = current.clone();
next.remove(&id);
next
}
fn voter_ids(view: &MembershipView) -> BTreeSet<u64> {
view.members
.iter()
.filter(|entry| entry.role == MemberRole::Voter)
.map(|entry| entry.id)
.collect()
}
pub(crate) struct OpenraftMembershipAdmin {
raft: Raft<TypeConfig, HighWaterStateMachine>,
host: std::sync::Arc<tsoracle_driver_openraft::StandaloneHost>,
source: std::sync::Arc<crate::drivers::openraft::network::PeerCapabilitySource>,
op_lock: Mutex<()>,
}
impl OpenraftMembershipAdmin {
pub(crate) fn new(
raft: Raft<TypeConfig, HighWaterStateMachine>,
host: std::sync::Arc<tsoracle_driver_openraft::StandaloneHost>,
source: std::sync::Arc<crate::drivers::openraft::network::PeerCapabilitySource>,
) -> Self {
Self {
raft,
host,
source,
op_lock: Mutex::new(()),
}
}
fn view(&self) -> MembershipView {
let metrics = self.raft.metrics().borrow_watched().clone();
let voters: BTreeSet<u64> = metrics.membership_config.voter_ids().collect();
let members = metrics
.membership_config
.nodes()
.map(|(id, node)| MemberEntry {
id: *id,
role: if voters.contains(id) {
MemberRole::Voter
} else {
MemberRole::Learner
},
raft_addr: node.addr.clone(),
service_endpoint: node.service_endpoint.clone(),
admin_endpoint: node.admin_endpoint.clone(),
})
.collect();
MembershipView {
members,
leader: metrics.current_leader,
}
}
}
#[async_trait]
impl MembershipAdmin for OpenraftMembershipAdmin {
async fn list_members(&self) -> Result<MembershipView, AdminError> {
Ok(self.view())
}
async fn add_learner(&self, member: NewMember) -> Result<(), AdminError> {
let _guard = self.op_lock.lock().await;
if self
.view()
.members
.iter()
.any(|entry| entry.id == member.id)
{
return Ok(());
}
let node = OpenraftPeer {
addr: member.raft_addr,
service_endpoint: member.service_endpoint,
admin_endpoint: member.admin_endpoint,
};
self.raft
.add_learner(member.id, node, true)
.await
.map(|_| ())
.map_err(map_write_error)
}
async fn promote(&self, id: u64) -> Result<(), AdminError> {
let _guard = self.op_lock.lock().await;
let view = self.view();
match view.members.iter().find(|entry| entry.id == id) {
None => return Err(AdminError::NotMember(id)),
Some(entry) if entry.role == MemberRole::Voter => return Ok(()), _ => {}
}
let next = voters_with(&voter_ids(&view), id);
self.raft
.change_membership(next, false)
.await
.map(|_| ())
.map_err(map_write_error)
}
async fn remove(&self, id: u64) -> Result<(), AdminError> {
let _guard = self.op_lock.lock().await;
let view = self.view();
if !view.members.iter().any(|entry| entry.id == id) {
return Ok(());
}
let voters = voter_ids(&view);
if voters.contains(&id) && voters.len() <= 1 {
return Err(AdminError::WouldLoseQuorum);
}
let next = voters_without(&voters, id);
self.raft
.change_membership(next, false)
.await
.map(|_| ())
.map_err(map_write_error)
}
async fn activate_format(&self, target: u8) -> Result<(), AdminError> {
self.host
.initiate_format_activation(target, &*self.source)
.await
.map_err(map_activation_error)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn voters_with_adds_the_id() {
let current = BTreeSet::from([1, 2, 3]);
assert_eq!(voters_with(¤t, 4), BTreeSet::from([1, 2, 3, 4]));
}
#[test]
fn voters_without_removes_the_id() {
let current = BTreeSet::from([1, 2, 3]);
assert_eq!(voters_without(¤t, 3), BTreeSet::from([1, 2]));
}
#[test]
fn forward_to_leader_maps_to_not_leader_with_admin_endpoint() {
let mut ftl = ForwardToLeader::<TypeConfig>::empty();
ftl.leader_node = Some(OpenraftPeer {
addr: "a:1".into(),
service_endpoint: "a:2".into(),
admin_endpoint: "a:3".into(),
});
let err = RaftError::APIError(ClientWriteError::ForwardToLeader(ftl));
match map_write_error(err) {
AdminError::NotLeader {
leader_admin_endpoint,
} => {
assert_eq!(leader_admin_endpoint.as_deref(), Some("a:3"));
}
other => panic!("expected NotLeader, got {other:?}"),
}
}
#[test]
fn forward_to_leader_with_no_node_has_no_endpoint() {
let err = RaftError::APIError(ClientWriteError::ForwardToLeader(ForwardToLeader::<
TypeConfig,
>::empty()));
match map_write_error(err) {
AdminError::NotLeader {
leader_admin_endpoint,
} => {
assert_eq!(leader_admin_endpoint, None);
}
other => panic!("expected NotLeader, got {other:?}"),
}
}
#[test]
fn learner_not_found_maps_to_not_member() {
use openraft::error::LearnerNotFound;
let err: RaftError<TypeConfig, ClientWriteError<TypeConfig>> =
RaftError::APIError(ClientWriteError::ChangeMembershipError(
ChangeMembershipError::LearnerNotFound(LearnerNotFound { node_id: 7 }),
));
assert!(matches!(map_write_error(err), AdminError::NotMember(7)));
}
#[test]
fn activation_not_leader_maps_to_not_leader_none_endpoint() {
use tsoracle_driver_openraft::FormatActivationError as FAE;
match map_activation_error(FAE::NotLeader) {
AdminError::NotLeader {
leader_admin_endpoint,
} => {
assert_eq!(leader_admin_endpoint, None);
}
other => panic!("expected NotLeader, got {other:?}"),
}
}
#[test]
fn activation_target_out_of_range_maps_correctly() {
use tsoracle_driver_openraft::FormatActivationError as FAE;
match map_activation_error(FAE::TargetOutOfRange {
target: 99,
min: 4,
max: 5,
}) {
AdminError::TargetOutOfRange { target, min, max } => {
assert_eq!((target, min, max), (99, 4, 5));
}
other => panic!("expected TargetOutOfRange, got {other:?}"),
}
}
#[test]
fn activation_members_below_target_maps_preserves_incapable() {
use tsoracle_driver_openraft::FormatActivationError as FAE;
match map_activation_error(FAE::MembersBelowTarget {
target: 5,
incapable: vec![(1, 4), (3, 4)],
}) {
AdminError::MembersBelowTarget { target, incapable } => {
assert_eq!(target, 5);
assert_eq!(incapable, vec![(1u64, 4u8), (3u64, 4u8)]);
}
other => panic!("expected MembersBelowTarget, got {other:?}"),
}
}
#[test]
fn activation_member_unreachable_maps_to_driver_with_detail() {
use tsoracle_driver_openraft::FormatActivationError as FAE;
match map_activation_error(FAE::MemberUnreachable {
node_id: 2,
detail: "timeout".into(),
}) {
AdminError::Driver(s) => {
assert!(s.contains("member 2"), "missing node id in {s:?}");
assert!(s.contains("timeout"), "missing detail in {s:?}");
}
other => panic!("expected Driver, got {other:?}"),
}
}
#[test]
fn activation_membership_changed_maps_correctly() {
use tsoracle_driver_openraft::FormatActivationError as FAE;
match map_activation_error(FAE::MembershipChangedSinceGate { target: 5 }) {
AdminError::MembershipChangedSinceGate { target } => {
assert_eq!(target, 5);
}
other => panic!("expected MembershipChangedSinceGate, got {other:?}"),
}
}
#[test]
fn activation_proposal_failed_maps_to_driver_with_detail() {
use tsoracle_driver_openraft::FormatActivationError as FAE;
match map_activation_error(FAE::ProposalFailed("write failed".into())) {
AdminError::Driver(s) => {
assert!(s.contains("write failed"), "missing detail in {s:?}");
}
other => panic!("expected Driver, got {other:?}"),
}
}
}