tsoracle_standalone/admin/
mod.rs1#[cfg(feature = "openraft")]
28pub(crate) mod openraft;
29
30#[cfg(feature = "openraft")]
31pub(crate) mod service;
32
33#[cfg(all(any(test, feature = "test-support"), feature = "openraft"))]
40pub mod test_support {
41 use std::sync::Arc;
42
43 use super::MembershipAdmin;
44 use crate::admin::service::AdminServiceImpl;
45 use crate::admin_proto::membership_admin_server::MembershipAdmin as GrpcAdmin;
46
47 pub fn admin_service(admin: Arc<dyn MembershipAdmin>) -> impl GrpcAdmin {
50 AdminServiceImpl::new(admin)
51 }
52}
53
54use async_trait::async_trait;
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum MemberRole {
59 Voter,
60 Learner,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct MemberEntry {
66 pub id: u64,
67 pub role: MemberRole,
68 pub raft_addr: String,
69 pub service_endpoint: String,
70 pub admin_endpoint: String,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct MembershipView {
76 pub members: Vec<MemberEntry>,
77 pub leader: Option<u64>,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct NewMember {
83 pub id: u64,
84 pub raft_addr: String,
85 pub service_endpoint: String,
86 pub admin_endpoint: String,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
92pub enum CapabilityState {
93 Reported {
96 min_readable: u8,
97 max_readable: u8,
98 active_write: u8,
99 },
100 Unreachable { detail: String },
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct MemberCapability {
109 pub member: MemberEntry,
110 pub caps: CapabilityState,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
117pub struct CapabilityReport {
118 pub members: Vec<MemberCapability>,
119 pub leader: Option<u64>,
120}
121
122#[derive(Debug, thiserror::Error)]
124pub enum AdminError {
125 #[error("not the leader")]
128 NotLeader {
129 leader_admin_endpoint: Option<String>,
130 },
131 #[error("membership changes are not supported by this driver")]
133 Unsupported,
134 #[error("node {0} is not a member")]
136 NotMember(u64),
137 #[error("node {0} has not caught up")]
139 NotCaughtUp(u64),
140 #[error("change would lose quorum")]
142 WouldLoseQuorum,
143 #[error("membership change timed out")]
145 Timeout,
146 #[error("driver error: {0}")]
148 Driver(String),
149 #[error("format activation to target {target} blocked: members below target: {incapable:?}")]
152 MembersBelowTarget {
153 target: u8,
154 incapable: Vec<(u64, u8)>,
155 },
156 #[error("format activation: target {target} outside readable range [{min}, {max}]")]
158 TargetOutOfRange { target: u8, min: u8, max: u8 },
159 #[error("format activation to target {target} no-op: membership changed since gate")]
161 MembershipChangedSinceGate { target: u8 },
162}
163
164#[async_trait]
166pub trait MembershipAdmin: Send + Sync {
167 async fn list_members(&self) -> Result<MembershipView, AdminError>;
168 async fn add_learner(&self, member: NewMember) -> Result<(), AdminError>;
169 async fn promote(&self, id: u64) -> Result<(), AdminError>;
170 async fn remove(&self, id: u64) -> Result<(), AdminError>;
171 async fn activate_format(&self, target: u8) -> Result<(), AdminError>;
177
178 async fn report_capabilities(&self) -> Result<CapabilityReport, AdminError>;
183}
184
185pub struct UnsupportedAdmin {
189 view: MembershipView,
190}
191
192impl UnsupportedAdmin {
193 pub fn new(view: MembershipView) -> Self {
194 Self { view }
195 }
196}
197
198#[async_trait]
199impl MembershipAdmin for UnsupportedAdmin {
200 async fn list_members(&self) -> Result<MembershipView, AdminError> {
201 Ok(self.view.clone())
202 }
203 async fn add_learner(&self, _member: NewMember) -> Result<(), AdminError> {
204 Err(AdminError::Unsupported)
205 }
206 async fn promote(&self, _id: u64) -> Result<(), AdminError> {
207 Err(AdminError::Unsupported)
208 }
209 async fn remove(&self, _id: u64) -> Result<(), AdminError> {
210 Err(AdminError::Unsupported)
211 }
212 async fn activate_format(&self, _target: u8) -> Result<(), AdminError> {
213 Err(AdminError::Unsupported)
214 }
215
216 async fn report_capabilities(&self) -> Result<CapabilityReport, AdminError> {
217 Err(AdminError::Unsupported)
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 fn empty_view() -> MembershipView {
226 MembershipView {
227 members: Vec::new(),
228 leader: None,
229 }
230 }
231
232 fn new_member() -> NewMember {
233 NewMember {
234 id: 2,
235 raft_addr: "127.0.0.1:9".into(),
236 service_endpoint: "127.0.0.1:8".into(),
237 admin_endpoint: "127.0.0.1:7".into(),
238 }
239 }
240
241 #[tokio::test]
242 async fn unsupported_admin_rejects_every_mutation() {
243 let admin = UnsupportedAdmin::new(empty_view());
244 assert!(matches!(
245 admin.add_learner(new_member()).await,
246 Err(AdminError::Unsupported)
247 ));
248 assert!(matches!(
249 admin.promote(2).await,
250 Err(AdminError::Unsupported)
251 ));
252 assert!(matches!(
253 admin.remove(2).await,
254 Err(AdminError::Unsupported)
255 ));
256 }
257
258 #[tokio::test]
259 async fn unsupported_admin_rejects_activate_format() {
260 let admin = UnsupportedAdmin::new(empty_view());
261 assert!(matches!(
262 admin.activate_format(5).await,
263 Err(AdminError::Unsupported)
264 ));
265 }
266
267 #[tokio::test]
268 async fn unsupported_admin_rejects_report_capabilities() {
269 let admin = UnsupportedAdmin::new(empty_view());
270 assert!(matches!(
271 admin.report_capabilities().await,
272 Err(AdminError::Unsupported)
273 ));
274 }
275
276 #[tokio::test]
277 async fn unsupported_admin_returns_its_fixed_view() {
278 let view = MembershipView {
279 members: vec![MemberEntry {
280 id: 1,
281 role: MemberRole::Voter,
282 raft_addr: "a:1".into(),
283 service_endpoint: "a:2".into(),
284 admin_endpoint: "a:3".into(),
285 }],
286 leader: Some(1),
287 };
288 let admin = UnsupportedAdmin::new(view.clone());
289 assert_eq!(admin.list_members().await.unwrap(), view);
290 }
291}