#[cfg(feature = "openraft")]
pub(crate) mod openraft;
#[cfg(feature = "openraft")]
pub(crate) mod service;
#[cfg(all(any(test, feature = "test-support"), feature = "openraft"))]
pub mod test_support {
use std::sync::Arc;
use super::MembershipAdmin;
use crate::admin::service::AdminServiceImpl;
use crate::admin_proto::membership_admin_server::MembershipAdmin as GrpcAdmin;
pub fn admin_service(admin: Arc<dyn MembershipAdmin>) -> impl GrpcAdmin {
AdminServiceImpl::new(admin)
}
}
use async_trait::async_trait;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MemberRole {
Voter,
Learner,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MemberEntry {
pub id: u64,
pub role: MemberRole,
pub raft_addr: String,
pub service_endpoint: String,
pub admin_endpoint: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MembershipView {
pub members: Vec<MemberEntry>,
pub leader: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NewMember {
pub id: u64,
pub raft_addr: String,
pub service_endpoint: String,
pub admin_endpoint: String,
}
#[derive(Debug, thiserror::Error)]
pub enum AdminError {
#[error("not the leader")]
NotLeader {
leader_admin_endpoint: Option<String>,
},
#[error("membership changes are not supported by this driver")]
Unsupported,
#[error("node {0} is not a member")]
NotMember(u64),
#[error("node {0} has not caught up")]
NotCaughtUp(u64),
#[error("change would lose quorum")]
WouldLoseQuorum,
#[error("membership change timed out")]
Timeout,
#[error("driver error: {0}")]
Driver(String),
#[error("format activation to target {target} blocked: members below target: {incapable:?}")]
MembersBelowTarget {
target: u8,
incapable: Vec<(u64, u8)>,
},
#[error("format activation: target {target} outside readable range [{min}, {max}]")]
TargetOutOfRange { target: u8, min: u8, max: u8 },
#[error("format activation to target {target} no-op: membership changed since gate")]
MembershipChangedSinceGate { target: u8 },
}
#[async_trait]
pub trait MembershipAdmin: Send + Sync {
async fn list_members(&self) -> Result<MembershipView, AdminError>;
async fn add_learner(&self, member: NewMember) -> Result<(), AdminError>;
async fn promote(&self, id: u64) -> Result<(), AdminError>;
async fn remove(&self, id: u64) -> Result<(), AdminError>;
async fn activate_format(&self, target: u8) -> Result<(), AdminError>;
}
pub struct UnsupportedAdmin {
view: MembershipView,
}
impl UnsupportedAdmin {
pub fn new(view: MembershipView) -> Self {
Self { view }
}
}
#[async_trait]
impl MembershipAdmin for UnsupportedAdmin {
async fn list_members(&self) -> Result<MembershipView, AdminError> {
Ok(self.view.clone())
}
async fn add_learner(&self, _member: NewMember) -> Result<(), AdminError> {
Err(AdminError::Unsupported)
}
async fn promote(&self, _id: u64) -> Result<(), AdminError> {
Err(AdminError::Unsupported)
}
async fn remove(&self, _id: u64) -> Result<(), AdminError> {
Err(AdminError::Unsupported)
}
async fn activate_format(&self, _target: u8) -> Result<(), AdminError> {
Err(AdminError::Unsupported)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn empty_view() -> MembershipView {
MembershipView {
members: Vec::new(),
leader: None,
}
}
fn new_member() -> NewMember {
NewMember {
id: 2,
raft_addr: "127.0.0.1:9".into(),
service_endpoint: "127.0.0.1:8".into(),
admin_endpoint: "127.0.0.1:7".into(),
}
}
#[tokio::test]
async fn unsupported_admin_rejects_every_mutation() {
let admin = UnsupportedAdmin::new(empty_view());
assert!(matches!(
admin.add_learner(new_member()).await,
Err(AdminError::Unsupported)
));
assert!(matches!(
admin.promote(2).await,
Err(AdminError::Unsupported)
));
assert!(matches!(
admin.remove(2).await,
Err(AdminError::Unsupported)
));
}
#[tokio::test]
async fn unsupported_admin_rejects_activate_format() {
let admin = UnsupportedAdmin::new(empty_view());
assert!(matches!(
admin.activate_format(5).await,
Err(AdminError::Unsupported)
));
}
#[tokio::test]
async fn unsupported_admin_returns_its_fixed_view() {
let view = MembershipView {
members: vec![MemberEntry {
id: 1,
role: MemberRole::Voter,
raft_addr: "a:1".into(),
service_endpoint: "a:2".into(),
admin_endpoint: "a:3".into(),
}],
leader: Some(1),
};
let admin = UnsupportedAdmin::new(view.clone());
assert_eq!(admin.list_members().await.unwrap(), view);
}
}