1mod deadline;
13mod exit_view;
14mod installation;
15mod poll;
16mod receive;
17mod storage;
18mod validation;
19
20pub use installation::ActivationTicket;
21pub use validation::ValidationTicket;
22
23use std::time::Duration;
24
25use ozzy_proto::NodeId;
26
27use crate::wire::Control;
28use crate::{
29 Configuration, Digest, InstallingView, LogSource, NormalReplica, OpNumber, PromiseTicket,
30 ReplicaSnapshot, ReplicationError, Scope, SelectedView, Status, ViewChange, ViewChangeError,
31 VoterIndex, VoterSet, Voters,
32};
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct Timing {
37 pub heartbeat: Duration,
39 pub primary_timeout: Duration,
41 pub retransmit: Duration,
43 pub election_timeout: Duration,
45 pub max_election_timeout: Duration,
47}
48
49impl Timing {
50 fn validate(self, now: Duration) -> Result<(), DriverError> {
51 if self.heartbeat.is_zero()
52 || self.retransmit.is_zero()
53 || self.heartbeat >= self.primary_timeout
54 || self.retransmit >= self.election_timeout
55 || self.election_timeout > self.max_election_timeout
56 {
57 return Err(DriverError::InvalidTiming);
58 }
59 if now
60 .checked_add(self.max_election_timeout.max(self.primary_timeout))
61 .is_none()
62 {
63 return Err(DriverError::TimeExhausted);
64 }
65 Ok(())
66 }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum Action {
72 Broadcast(Control),
74 Send {
76 to: NodeId,
78 message: Control,
80 },
81 PersistPromise(PromiseTicket),
83}
84
85#[derive(Debug)]
87#[expect(
88 clippy::large_enum_variant,
89 reason = "move preallocated roles without boxing"
90)]
91pub enum Role {
92 Normal(NormalReplica),
94 ViewChanging(ViewChange),
96 Installing(InstallingView),
98 Fenced(Scope),
101}
102
103#[derive(Debug)]
108pub struct ReplicaDriver {
109 role: Role,
110 configuration: Configuration,
111 local: VoterIndex,
112 timing: Timing,
113 now: Duration,
114 contact_deadline: Duration,
115 progress_deadline: Duration,
116 persistence_deadline: Duration,
117 election_deadline: Duration,
118 election_delay: Duration,
119 retry_at: Duration,
120 start_pending: bool,
121 report_pending: bool,
122 commit_pending: bool,
123 activation_pending: VoterSet,
126 exit_votes: VoterSet,
128 exit_retry_at: Option<Duration>,
130 exit_echo: Option<(Scope, VoterSet)>,
133 exit_echo_at: Voters<Duration>,
135}
136
137impl ReplicaDriver {
138 pub fn from_normal(
141 normal: NormalReplica,
142 now: Duration,
143 timing: Timing,
144 ) -> Result<Self, DriverError> {
145 let (configuration, local) = normal.driver_identity();
146 Self::new(Role::Normal(normal), configuration, local, now, timing)
147 }
148
149 pub fn from_view_change(
152 changing: ViewChange,
153 now: Duration,
154 timing: Timing,
155 ) -> Result<Self, DriverError> {
156 let configuration = changing.configuration;
157 let local = changing.local;
158 Self::new(
159 Role::ViewChanging(changing),
160 configuration,
161 local,
162 now,
163 timing,
164 )
165 }
166
167 fn new(
168 role: Role,
169 configuration: Configuration,
170 local: VoterIndex,
171 now: Duration,
172 timing: Timing,
173 ) -> Result<Self, DriverError> {
174 timing.validate(now)?;
175 let changing = matches!(role, Role::ViewChanging(_));
176 let activating =
177 matches!(&role, Role::Normal(normal) if !normal.snapshot().ready_for_appends);
178 Ok(Self {
179 role,
180 configuration,
181 local,
182 timing,
183 now,
184 contact_deadline: now.saturating_add(timing.primary_timeout),
185 persistence_deadline: now.saturating_add(timing.primary_timeout),
186 progress_deadline: now
187 + if activating {
188 timing.election_timeout
189 } else {
190 timing.primary_timeout
191 },
192 election_deadline: now.saturating_add(timing.election_timeout),
193 election_delay: timing.election_timeout,
194 retry_at: now
195 + if changing {
196 timing.retransmit
197 } else {
198 timing.heartbeat
199 },
200 start_pending: changing,
201 report_pending: changing,
202 commit_pending: false,
203 activation_pending: VoterSet::EMPTY,
204 exit_votes: VoterSet::EMPTY,
205 exit_retry_at: None,
206 exit_echo: None,
207 exit_echo_at: Voters::splat(Duration::ZERO),
208 })
209 }
210
211 pub fn normal(&self) -> Option<&NormalReplica> {
213 match &self.role {
214 Role::Normal(normal) => Some(normal),
215 Role::ViewChanging(_) | Role::Installing(_) | Role::Fenced(_) => None,
216 }
217 }
218
219 pub fn election_needs_sync(&self) -> bool {
221 let Role::ViewChanging(changing) = &self.role else {
222 return false;
223 };
224 let journal = changing.normal_snapshot().journal;
225 journal.written == journal.accepted && journal.written > journal.durable
226 }
227
228 pub fn scope(&self) -> Scope {
230 match &self.role {
231 Role::Normal(normal) => normal.scope(),
232 Role::ViewChanging(changing) => changing.scope(),
233 Role::Installing(installing) => installing.requested_scope(),
234 Role::Fenced(scope) => *scope,
235 }
236 }
237
238 pub fn into_role(self) -> Role {
241 self.role
242 }
243
244 pub fn select(
247 &mut self,
248 lookup: impl FnMut(LogSource, OpNumber) -> Option<Digest>,
249 ) -> Result<SelectedView, DriverError> {
250 Ok(self.changing()?.select(lookup)?)
251 }
252
253 pub fn history_sources(&self) -> [Option<LogSource>; 3] {
255 match &self.role {
256 Role::ViewChanging(changing) => changing.history_sources(),
257 Role::Normal(_) | Role::Installing(_) | Role::Fenced(_) => [None; 3],
258 }
259 }
260
261 fn changing(&mut self) -> Result<&mut ViewChange, DriverError> {
262 match &mut self.role {
263 Role::ViewChanging(changing) => Ok(changing),
264 Role::Normal(_) | Role::Installing(_) => Err(ReplicationError::WrongRole.into()),
265 Role::Fenced(_) => Err(ViewChangeError::Faulted.into()),
266 }
267 }
268
269 fn require_normal(&self) -> Result<&NormalReplica, DriverError> {
270 Ok(self.normal().ok_or(ReplicationError::NotNormal)?)
271 }
272
273 fn require_healthy(&self) -> Result<(), DriverError> {
275 match &self.role {
276 Role::Normal(normal) if normal.snapshot().status == Status::Faulted => {
277 Err(ViewChangeError::Faulted.into())
278 }
279 Role::ViewChanging(changing)
280 if changing.normal_snapshot().status == Status::Faulted =>
281 {
282 Err(ViewChangeError::Faulted.into())
283 }
284 Role::Installing(installing) => Ok(installing.require_pending()?),
285 Role::Fenced(_) => Err(ViewChangeError::Faulted.into()),
286 Role::Normal(_) | Role::ViewChanging(_) => Ok(()),
287 }
288 }
289
290 fn take_role(&mut self) -> Role {
293 let fenced = Role::Fenced(self.scope());
294 std::mem::replace(&mut self.role, fenced)
295 }
296
297 fn observe_time(&mut self, now: Duration) -> Result<(), DriverError> {
298 if now < self.now {
299 return Err(DriverError::ClockRegressed);
300 }
301 self.timing.validate(now)?;
302 self.now = now;
303 Ok(())
304 }
305
306 fn progress_timeout(&self) -> Duration {
307 if self
308 .normal()
309 .is_some_and(|normal| normal.snapshot().ready_for_appends)
310 {
311 self.timing.primary_timeout
312 } else {
313 self.election_delay
314 }
315 }
316
317 fn observe_normal_progress(
318 &mut self,
319 previous: &ReplicaSnapshot,
320 current: &ReplicaSnapshot,
321 now: Duration,
322 ) {
323 if current.committed != previous.committed {
324 self.progress_deadline = now.saturating_add(self.progress_timeout());
325 }
326 if current.journal.durable > previous.journal.durable {
327 self.persistence_deadline = now.saturating_add(self.timing.primary_timeout);
328 }
329 }
330
331 fn normal_timed_out(&self, snapshot: &ReplicaSnapshot) -> bool {
332 let primary = VoterIndex::primary(snapshot.scope.view) == self.local;
333 let outstanding = snapshot.accepted != snapshot.committed || !snapshot.ready_for_appends;
334 (!primary && self.now >= self.contact_deadline)
335 || (outstanding && self.now >= self.progress_deadline)
336 || (snapshot.accepted.op > snapshot.journal.durable
337 && self.now >= self.persistence_deadline)
338 }
339
340 fn next_view(&mut self) -> Result<(), DriverError> {
341 let view = self
342 .scope()
343 .view
344 .checked_add(1)
345 .ok_or(ViewChangeError::ViewExhausted)?;
346 self.change_view(view)
347 }
348
349 fn change_view(&mut self, view: u64) -> Result<(), DriverError> {
350 match &mut self.role {
351 Role::ViewChanging(changing) => changing.advance_view(view)?,
352 Role::Installing(installing) => installing.request_view(view)?,
353 Role::Normal(normal) if normal.snapshot().status == Status::Faulted => {
354 return Err(ViewChangeError::Faulted.into());
355 }
356 Role::Normal(_) => {
357 let Role::Normal(normal) = self.take_role() else {
358 return Err(ViewChangeError::Faulted.into());
359 };
360 self.role = Role::ViewChanging(normal.into_view_change(view)?);
361 }
362 Role::Fenced(_) => return Err(ViewChangeError::Faulted.into()),
363 }
364 self.exit_votes = VoterSet::EMPTY;
365 self.exit_retry_at = None;
366 self.election_deadline = self.now.saturating_add(self.election_delay);
367 self.retry_at = self.now.saturating_add(self.timing.retransmit);
368 self.start_pending = true;
369 self.report_pending = true;
370 self.commit_pending = false;
371 self.activation_pending = VoterSet::EMPTY;
372 Ok(())
373 }
374}
375
376#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
378pub enum DriverError {
379 #[error("invalid replica timing intervals")]
381 InvalidTiming,
382 #[error("replica clock regressed")]
384 ClockRegressed,
385 #[error("replica clock exhausted")]
387 TimeExhausted,
388 #[error("stale replica application validation")]
390 StaleValidation,
391 #[error(transparent)]
393 Replication(#[from] ReplicationError),
394 #[error(transparent)]
396 ViewChange(#[from] ViewChangeError),
397}