1use std::sync::Arc;
10use std::time::{Duration, Instant};
11
12use crate::member::{Member, MemberStatus};
13use crate::membership::MembershipState;
14use crate::sbr::{DowningDecision, DowningStrategy, QuorumObserver};
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18#[non_exhaustive]
19pub enum SbrAction {
20 None,
22 DownUnreachable(Vec<String>),
24 DownAll(Vec<String>),
26 DownSelf,
28}
29
30pub struct SbrRuntime<S: DowningStrategy> {
32 strategy: S,
33 stable_after: Duration,
34 unstable_since: Option<Instant>,
37 observer: Option<Arc<dyn QuorumObserver>>,
41 quorum_lost: bool,
44}
45
46impl<S: DowningStrategy> SbrRuntime<S> {
47 pub fn new(strategy: S, stable_after: Duration) -> Self {
48 Self { strategy, stable_after, unstable_since: None, observer: None, quorum_lost: false }
49 }
50
51 pub fn with_observer(mut self, observer: Arc<dyn QuorumObserver>) -> Self {
54 self.observer = Some(observer);
55 self
56 }
57
58 fn note_quorum_lost(&mut self) {
60 if !self.quorum_lost {
61 self.quorum_lost = true;
62 if let Some(o) = &self.observer {
63 o.on_quorum_lost();
64 }
65 }
66 }
67
68 fn note_quorum_regained(&mut self) {
71 if self.quorum_lost {
72 self.quorum_lost = false;
73 if let Some(o) = &self.observer {
74 o.on_quorum_regained();
75 }
76 }
77 }
78
79 pub fn tick(&mut self, state: &MembershipState, now: Instant) -> SbrAction {
82 let mut reachable: Vec<&Member> = Vec::new();
84 let mut unreachable: Vec<&Member> = Vec::new();
85 for m in &state.members {
86 if matches!(m.status, MemberStatus::Down | MemberStatus::Removed) {
87 continue;
88 }
89 if state.reachability.is_reachable(&m.address) {
90 reachable.push(m);
91 } else {
92 unreachable.push(m);
93 }
94 }
95
96 if unreachable.is_empty() {
97 self.unstable_since = None;
100 self.note_quorum_regained();
101 return SbrAction::None;
102 }
103
104 let since = *self.unstable_since.get_or_insert(now);
106 if now.duration_since(since) < self.stable_after {
107 return SbrAction::None;
108 }
109
110 match self.strategy.decide(&reachable, &unreachable) {
111 DowningDecision::Stay => SbrAction::None,
112 DowningDecision::DownUnreachable => {
113 SbrAction::DownUnreachable(unreachable.iter().map(|m| m.address.to_string()).collect())
114 }
115 DowningDecision::DownAll => {
116 SbrAction::DownAll(state.members.iter().map(|m| m.address.to_string()).collect())
117 }
118 DowningDecision::DownSelf => {
119 self.note_quorum_lost();
122 SbrAction::DownSelf
123 }
124 }
125 }
126
127 pub fn is_stable(&self, now: Instant) -> bool {
130 match self.unstable_since {
131 Some(t) => now.duration_since(t) >= self.stable_after,
132 None => true, }
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use crate::sbr::KeepMajorityStrategy;
141 use atomr_core::actor::Address;
142
143 fn member(addr: &str, status: MemberStatus) -> Member {
144 let mut m = Member::new(Address::local(addr), vec![]);
145 m.status = status;
146 m
147 }
148
149 #[test]
150 fn none_when_no_partition() {
151 let mut s = MembershipState::new();
152 s.add_or_update(member("a", MemberStatus::Up));
153 s.add_or_update(member("b", MemberStatus::Up));
154 let mut rt = SbrRuntime::new(KeepMajorityStrategy, Duration::from_millis(0));
155 let r = rt.tick(&s, Instant::now());
156 assert_eq!(r, SbrAction::None);
157 assert!(rt.is_stable(Instant::now()));
158 }
159
160 #[test]
161 fn waits_for_stability_window() {
162 let mut s = MembershipState::new();
163 s.add_or_update(member("a", MemberStatus::Up));
164 s.add_or_update(member("b", MemberStatus::Up));
165 s.add_or_update(member("c", MemberStatus::Up));
166 s.reachability.unreachable(Address::local("b"), Address::local("c"));
167 let mut rt = SbrRuntime::new(KeepMajorityStrategy, Duration::from_secs(60));
168 let now = Instant::now();
169 assert_eq!(rt.tick(&s, now), SbrAction::None);
171 assert!(!rt.is_stable(now));
172 }
173
174 #[test]
175 fn fires_after_stability_window_with_majority() {
176 let mut s = MembershipState::new();
177 s.add_or_update(member("a", MemberStatus::Up));
178 s.add_or_update(member("b", MemberStatus::Up));
179 s.add_or_update(member("c", MemberStatus::Up));
180 s.reachability.unreachable(Address::local("b"), Address::local("c"));
182 let mut rt = SbrRuntime::new(KeepMajorityStrategy, Duration::from_millis(0));
183 let r = rt.tick(&s, Instant::now());
184 match r {
185 SbrAction::DownUnreachable(addrs) => {
186 assert_eq!(addrs, vec!["akka://c".to_string()]);
187 }
188 other => panic!("expected DownUnreachable, got {other:?}"),
189 }
190 }
191
192 #[test]
193 fn quorum_observer_fires_on_loss_and_regain() {
194 use crate::sbr::{QuorumObserver, StaticQuorumStrategy};
195 use std::sync::atomic::{AtomicU32, Ordering};
196
197 struct Counter {
198 lost: AtomicU32,
199 regained: AtomicU32,
200 }
201 impl QuorumObserver for Counter {
202 fn on_quorum_lost(&self) {
203 self.lost.fetch_add(1, Ordering::SeqCst);
204 }
205 fn on_quorum_regained(&self) {
206 self.regained.fetch_add(1, Ordering::SeqCst);
207 }
208 }
209
210 let obs = Arc::new(Counter { lost: AtomicU32::new(0), regained: AtomicU32::new(0) });
211
212 let mut s = MembershipState::new();
213 s.add_or_update(member("a", MemberStatus::Up));
214 s.add_or_update(member("b", MemberStatus::Up));
215 s.add_or_update(member("c", MemberStatus::Up));
216 s.reachability.unreachable(Address::local("a"), Address::local("b"));
219 s.reachability.unreachable(Address::local("a"), Address::local("c"));
220
221 let mut rt = SbrRuntime::new(StaticQuorumStrategy { quorum_size: 3 }, Duration::from_millis(0))
223 .with_observer(obs.clone());
224 let now = Instant::now();
225 assert_eq!(rt.tick(&s, now), SbrAction::DownSelf);
226 assert_eq!(obs.lost.load(Ordering::SeqCst), 1);
227 assert_eq!(rt.tick(&s, now), SbrAction::DownSelf);
229 assert_eq!(obs.lost.load(Ordering::SeqCst), 1);
230 assert_eq!(obs.regained.load(Ordering::SeqCst), 0);
231
232 s.reachability.reachable(Address::local("a"), Address::local("b"));
234 s.reachability.reachable(Address::local("a"), Address::local("c"));
235 assert_eq!(rt.tick(&s, now + Duration::from_secs(1)), SbrAction::None);
236 assert_eq!(obs.regained.load(Ordering::SeqCst), 1);
237 assert_eq!(rt.tick(&s, now + Duration::from_secs(2)), SbrAction::None);
239 assert_eq!(obs.regained.load(Ordering::SeqCst), 1);
240 }
241
242 #[test]
243 fn resets_clock_when_partition_heals() {
244 let mut s = MembershipState::new();
245 s.add_or_update(member("a", MemberStatus::Up));
246 s.add_or_update(member("b", MemberStatus::Up));
247 s.reachability.unreachable(Address::local("a"), Address::local("b"));
248 let mut rt = SbrRuntime::new(KeepMajorityStrategy, Duration::from_secs(60));
249 let now = Instant::now();
250 let _ = rt.tick(&s, now);
251 s.reachability.reachable(Address::local("a"), Address::local("b"));
253 let r = rt.tick(&s, now + Duration::from_secs(1));
254 assert_eq!(r, SbrAction::None);
255 assert!(rt.is_stable(now + Duration::from_secs(1)));
256 }
257}