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, CapabilityReport, CapabilityState, MemberCapability, 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()
}
fn assemble_report(
leader: Option<u64>,
voters: &BTreeSet<u64>,
members: &[(u64, OpenraftPeer)],
caps: &[(
u64,
Result<tsoracle_driver_openraft::NodeCapabilities, String>,
)],
) -> CapabilityReport {
let caps_by_id: std::collections::HashMap<
u64,
&Result<tsoracle_driver_openraft::NodeCapabilities, String>,
> = caps.iter().map(|(id, result)| (*id, result)).collect();
let members = members
.iter()
.map(|(id, node)| {
let caps = match caps_by_id.get(id) {
Some(Ok(capability)) => CapabilityState::Reported {
min_readable: capability.min_readable_version,
max_readable: capability.max_readable_version,
active_write: capability.active_write_version,
},
Some(Err(detail)) => CapabilityState::Unreachable {
detail: detail.clone(),
},
None => CapabilityState::Unreachable {
detail: "no capability report".to_string(),
},
};
MemberCapability {
member: 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(),
},
caps,
}
})
.collect();
CapabilityReport { members, leader }
}
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)
}
async fn report_capabilities(&self) -> Result<CapabilityReport, AdminError> {
let (leader, voters, local_id, members) = {
let metrics = self.raft.metrics().borrow_watched().clone();
let voters: BTreeSet<u64> = metrics.membership_config.voter_ids().collect();
let members: Vec<(u64, OpenraftPeer)> = metrics
.membership_config
.nodes()
.map(|(id, node)| (*id, node.clone()))
.collect();
(metrics.current_leader, voters, metrics.id, members)
};
let local_capabilities =
tsoracle_driver_openraft::NodeCapabilities::local(self.host.active_write_version());
let caps = tsoracle_driver_openraft::report_with(
local_id,
local_capabilities,
&members,
&*self.source,
)
.await;
Ok(assemble_report(leader, &voters, &members, &caps))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn peer(addr: &str) -> OpenraftPeer {
OpenraftPeer {
addr: format!("{addr}:1"),
service_endpoint: format!("{addr}:2"),
admin_endpoint: format!("{addr}:3"),
}
}
fn ncaps(min: u8, max: u8, active: u8) -> tsoracle_driver_openraft::NodeCapabilities {
tsoracle_driver_openraft::NodeCapabilities {
min_readable_version: min,
max_readable_version: max,
active_write_version: active,
}
}
#[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 assemble_report_builds_one_row_per_member_with_roles_and_caps() {
let members = vec![(1u64, peer("a")), (2u64, peer("b"))];
let voters = BTreeSet::from([1u64]);
let caps = vec![(1u64, Ok(ncaps(4, 6, 4))), (2u64, Ok(ncaps(4, 5, 4)))];
let report = assemble_report(Some(1), &voters, &members, &caps);
assert_eq!(report.leader, Some(1));
assert_eq!(report.members.len(), 2);
assert_eq!(report.members[0].member.id, 1);
assert_eq!(report.members[0].member.role, MemberRole::Voter);
assert_eq!(
report.members[0].caps,
crate::admin::CapabilityState::Reported {
min_readable: 4,
max_readable: 6,
active_write: 4,
}
);
assert_eq!(report.members[1].member.role, MemberRole::Learner);
assert_eq!(report.members[1].member.raft_addr, "b:1");
}
#[test]
fn assemble_report_marks_failed_query_unreachable_with_detail() {
let members = vec![(1u64, peer("a")), (2u64, peer("b"))];
let voters = BTreeSet::from([1u64, 2u64]);
let caps = vec![
(1u64, Ok(ncaps(4, 6, 4))),
(2u64, Err("connection refused".to_string())),
];
let report = assemble_report(None, &voters, &members, &caps);
assert_eq!(report.leader, None);
assert_eq!(
report.members[1].caps,
crate::admin::CapabilityState::Unreachable {
detail: "connection refused".to_string()
}
);
}
#[test]
fn assemble_report_marks_member_absent_from_caps_unreachable() {
let members = vec![(1u64, peer("a")), (2u64, peer("b"))];
let voters = BTreeSet::from([1u64, 2u64]);
let caps = vec![(1u64, Ok(ncaps(4, 6, 4)))];
let report = assemble_report(None, &voters, &members, &caps);
assert_eq!(report.members.len(), 2);
assert!(matches!(
report.members[1].caps,
crate::admin::CapabilityState::Unreachable { .. }
));
}
#[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:?}"),
}
}
}