1use crate::member::{Member, MemberStatus};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14#[non_exhaustive]
15pub enum DowningDecision {
16 DownUnreachable,
17 DownAll,
18 DownSelf,
19 Stay,
20}
21
22pub trait DowningStrategy: Send + Sync {
23 fn decide(&self, reachable: &[&Member], unreachable: &[&Member]) -> DowningDecision;
24}
25
26#[derive(Debug, Clone, Copy, Default)]
28pub struct KeepMajorityStrategy;
29
30impl DowningStrategy for KeepMajorityStrategy {
31 fn decide(&self, r: &[&Member], u: &[&Member]) -> DowningDecision {
32 let up = |ms: &[&Member]| ms.iter().filter(|m| m.status == MemberStatus::Up).count();
33 let rn = up(r);
34 let un = up(u);
35 if rn > un {
36 DowningDecision::DownUnreachable
37 } else if rn < un {
38 DowningDecision::DownSelf
39 } else {
40 DowningDecision::DownAll
41 }
42 }
43}
44
45#[derive(Debug, Clone, Copy)]
47pub struct StaticQuorumStrategy {
48 pub quorum_size: usize,
49}
50
51impl DowningStrategy for StaticQuorumStrategy {
52 fn decide(&self, r: &[&Member], _: &[&Member]) -> DowningDecision {
53 if r.len() >= self.quorum_size {
54 DowningDecision::DownUnreachable
55 } else {
56 DowningDecision::DownSelf
57 }
58 }
59}
60
61#[derive(Debug, Clone, Copy, Default)]
63pub struct KeepOldestStrategy {
64 pub down_if_alone: bool,
65}
66
67impl DowningStrategy for KeepOldestStrategy {
68 fn decide(&self, r: &[&Member], u: &[&Member]) -> DowningDecision {
69 fn oldest<'a>(ms: &[&'a Member]) -> Option<&'a Member> {
70 ms.iter().min_by_key(|m| m.up_number).copied()
71 }
72 let rolds = oldest(r);
73 let uolds = oldest(u);
74 match (rolds, uolds) {
75 (Some(ro), Some(uo)) => {
76 if ro.up_number <= uo.up_number {
77 if r.len() == 1 && self.down_if_alone {
78 DowningDecision::DownAll
79 } else {
80 DowningDecision::DownUnreachable
81 }
82 } else {
83 DowningDecision::DownSelf
84 }
85 }
86 (Some(_), None) => DowningDecision::DownUnreachable,
87 (None, Some(_)) => DowningDecision::DownSelf,
88 (None, None) => DowningDecision::Stay,
89 }
90 }
91}
92
93#[derive(Debug, Clone)]
95pub struct KeepReferee {
96 pub referee: String,
97 pub down_all_if_less_than: usize,
98}
99
100impl DowningStrategy for KeepReferee {
101 fn decide(&self, r: &[&Member], _u: &[&Member]) -> DowningDecision {
102 let has_referee = r.iter().any(|m| m.address.to_string() == self.referee);
103 if !has_referee {
104 return DowningDecision::DownSelf;
105 }
106 if r.len() < self.down_all_if_less_than {
107 DowningDecision::DownAll
108 } else {
109 DowningDecision::DownUnreachable
110 }
111 }
112}
113
114#[derive(Debug, Clone, Copy, Default)]
124pub struct DownAllStrategy;
125
126impl DowningStrategy for DownAllStrategy {
127 fn decide(&self, _r: &[&Member], u: &[&Member]) -> DowningDecision {
128 if u.is_empty() {
129 DowningDecision::Stay
130 } else {
131 DowningDecision::DownAll
132 }
133 }
134}
135
136#[derive(Debug, Clone, Copy, Default)]
139pub struct LeaseMajorityStrategy {
140 pub lease_acquired: bool,
141}
142
143impl DowningStrategy for LeaseMajorityStrategy {
144 fn decide(&self, r: &[&Member], u: &[&Member]) -> DowningDecision {
145 let m = KeepMajorityStrategy.decide(r, u);
146 match m {
147 DowningDecision::DownAll if self.lease_acquired => DowningDecision::DownUnreachable,
148 other => other,
149 }
150 }
151}
152
153#[derive(Debug, Clone, Default)]
166pub struct RoleWeightedQuorum {
167 pub weights: std::collections::HashMap<String, u32>,
168 pub min_quorum_weight: u32,
169}
170
171impl RoleWeightedQuorum {
172 pub fn new(weights: std::collections::HashMap<String, u32>, min_quorum_weight: u32) -> Self {
173 Self { weights, min_quorum_weight }
174 }
175
176 fn member_weight(&self, m: &Member) -> u32 {
179 m.roles.iter().filter_map(|r| self.weights.get(r).copied()).max().unwrap_or(0)
180 }
181}
182
183impl DowningStrategy for RoleWeightedQuorum {
184 fn decide(&self, r: &[&Member], _u: &[&Member]) -> DowningDecision {
185 let reachable_weight: u32 =
186 r.iter().filter(|m| m.status == MemberStatus::Up).map(|m| self.member_weight(m)).sum();
187 if reachable_weight >= self.min_quorum_weight {
188 DowningDecision::DownUnreachable
189 } else {
190 DowningDecision::DownSelf
191 }
192 }
193}
194
195pub trait QuorumObserver: Send + Sync {
203 fn on_quorum_lost(&self);
204 fn on_quorum_regained(&self);
205}
206
207pub struct SplitBrainResolver {
209 pub strategy: Box<dyn DowningStrategy>,
210}
211
212impl SplitBrainResolver {
213 pub fn new(strategy: Box<dyn DowningStrategy>) -> Self {
214 Self { strategy }
215 }
216 pub fn decide(&self, r: &[&Member], u: &[&Member]) -> DowningDecision {
217 self.strategy.decide(r, u)
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use atomr_core::actor::Address;
225
226 fn up(n: i32) -> Member {
227 let mut m = Member::new(Address::local(format!("N{n}")), vec![]);
228 m.status = MemberStatus::Up;
229 m.up_number = n;
230 m
231 }
232
233 fn up_roles(n: i32, roles: &[&str]) -> Member {
234 let mut m =
235 Member::new(Address::local(format!("N{n}")), roles.iter().map(|s| s.to_string()).collect());
236 m.status = MemberStatus::Up;
237 m.up_number = n;
238 m
239 }
240
241 #[test]
242 fn keep_majority_prefers_larger_side() {
243 let r = [up(1), up(2), up(3)];
244 let u = [up(4)];
245 let r_ref: Vec<&Member> = r.iter().collect();
246 let u_ref: Vec<&Member> = u.iter().collect();
247 assert_eq!(KeepMajorityStrategy.decide(&r_ref, &u_ref), DowningDecision::DownUnreachable);
248 }
249
250 #[test]
251 fn static_quorum_enforces_size() {
252 let r = [up(1)];
253 let u = [up(2)];
254 let r_ref: Vec<&Member> = r.iter().collect();
255 let u_ref: Vec<&Member> = u.iter().collect();
256 assert_eq!(StaticQuorumStrategy { quorum_size: 2 }.decide(&r_ref, &u_ref), DowningDecision::DownSelf);
257 }
258
259 #[test]
260 fn keep_oldest_picks_lowest_up_number() {
261 let r = [up(1)];
262 let u = [up(2), up(3)];
263 let r_ref: Vec<&Member> = r.iter().collect();
264 let u_ref: Vec<&Member> = u.iter().collect();
265 assert_eq!(KeepOldestStrategy::default().decide(&r_ref, &u_ref), DowningDecision::DownUnreachable);
266 }
267
268 #[test]
269 fn down_all_strategy_downs_every_member_when_partitioned() {
270 let r = [up(1), up(2)];
271 let u = [up(3)];
272 let r_ref: Vec<&Member> = r.iter().collect();
273 let u_ref: Vec<&Member> = u.iter().collect();
274 assert_eq!(DownAllStrategy.decide(&r_ref, &u_ref), DowningDecision::DownAll);
275 }
276
277 #[test]
278 fn down_all_strategy_stays_when_no_unreachable() {
279 let r = [up(1), up(2), up(3)];
280 let u: [Member; 0] = [];
281 let r_ref: Vec<&Member> = r.iter().collect();
282 let u_ref: Vec<&Member> = u.iter().collect();
283 assert_eq!(DownAllStrategy.decide(&r_ref, &u_ref), DowningDecision::Stay);
284 }
285
286 #[test]
287 fn role_weighted_downs_larger_but_lightweight_side() {
288 let mut weights = std::collections::HashMap::new();
293 weights.insert("core".to_string(), 10);
294 weights.insert("worker".to_string(), 1);
295 let q = RoleWeightedQuorum::new(weights, 10);
296
297 let r = [up_roles(1, &["worker"]), up_roles(2, &["worker"]), up_roles(3, &["worker"])];
298 let u = [up_roles(4, &["core"])];
299 let r_ref: Vec<&Member> = r.iter().collect();
300 let u_ref: Vec<&Member> = u.iter().collect();
301 assert_eq!(q.decide(&r_ref, &u_ref), DowningDecision::DownSelf);
302 }
303
304 #[test]
305 fn role_weighted_keeps_side_with_core_weight() {
306 let mut weights = std::collections::HashMap::new();
307 weights.insert("core".to_string(), 10);
308 weights.insert("worker".to_string(), 1);
309 let q = RoleWeightedQuorum::new(weights, 10);
310
311 let r = [up_roles(1, &["core"]), up_roles(2, &["worker"])];
313 let u = [up_roles(3, &["worker"]), up_roles(4, &["worker"]), up_roles(5, &["worker"])];
314 let r_ref: Vec<&Member> = r.iter().collect();
315 let u_ref: Vec<&Member> = u.iter().collect();
316 assert_eq!(q.decide(&r_ref, &u_ref), DowningDecision::DownUnreachable);
317 }
318
319 #[test]
320 fn role_weighted_member_weight_is_max_of_roles() {
321 let mut weights = std::collections::HashMap::new();
323 weights.insert("core".to_string(), 10);
324 weights.insert("worker".to_string(), 1);
325 let q = RoleWeightedQuorum::new(weights, 10);
326 let m = up_roles(1, &["core", "worker"]);
327 assert_eq!(q.member_weight(&m), 10);
328 let plain = up_roles(2, &["other"]);
330 assert_eq!(q.member_weight(&plain), 0);
331 }
332
333 #[test]
334 fn down_all_strategy_downs_even_with_majority_reachable() {
335 let r = [up(1), up(2), up(3), up(4)];
338 let u = [up(5)];
339 let r_ref: Vec<&Member> = r.iter().collect();
340 let u_ref: Vec<&Member> = u.iter().collect();
341 assert_eq!(DownAllStrategy.decide(&r_ref, &u_ref), DowningDecision::DownAll);
342 }
343}