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, Prefix,
30 PromiseTicket, ReplicaSnapshot, ReplicationError, Scope, SelectedView, Status, ViewChange,
31 ViewChangeError, 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 storage_busy_since: Option<Duration>,
119 election_deadline: Duration,
120 election_delay: Duration,
121 retry_at: Duration,
122 start_pending: bool,
123 report_pending: bool,
124 commit_pending: bool,
125 activation_pending: VoterSet,
128 exit_votes: VoterSet,
130 exit_heard_at: Voters<Duration>,
132 exit_retry_at: Option<Duration>,
134 exit_echo: Option<(Scope, VoterSet)>,
137 exit_echo_at: Voters<Duration>,
139 fence_answers: VoterSet,
142 stale_answers: VoterSet,
145}
146
147impl ReplicaDriver {
148 pub fn from_normal(
151 normal: NormalReplica,
152 now: Duration,
153 timing: Timing,
154 ) -> Result<Self, DriverError> {
155 let (configuration, local) = normal.driver_identity();
156 Self::new(Role::Normal(normal), configuration, local, now, timing)
157 }
158
159 pub fn from_view_change(
162 changing: ViewChange,
163 now: Duration,
164 timing: Timing,
165 ) -> Result<Self, DriverError> {
166 let configuration = changing.configuration;
167 let local = changing.local;
168 Self::new(
169 Role::ViewChanging(changing),
170 configuration,
171 local,
172 now,
173 timing,
174 )
175 }
176
177 fn new(
178 role: Role,
179 configuration: Configuration,
180 local: VoterIndex,
181 now: Duration,
182 timing: Timing,
183 ) -> Result<Self, DriverError> {
184 timing.validate(now)?;
185 let changing = matches!(role, Role::ViewChanging(_));
186 let activating =
187 matches!(&role, Role::Normal(normal) if !normal.snapshot().ready_for_appends);
188 Ok(Self {
189 role,
190 configuration,
191 local,
192 timing,
193 now,
194 contact_deadline: now.saturating_add(timing.primary_timeout),
195 persistence_deadline: now.saturating_add(timing.primary_timeout),
196 storage_busy_since: None,
197 progress_deadline: now
198 + if activating {
199 timing.election_timeout
200 } else {
201 timing.primary_timeout
202 },
203 election_deadline: now.saturating_add(timing.election_timeout),
204 election_delay: timing.election_timeout,
205 retry_at: now
206 + if changing {
207 timing.retransmit
208 } else {
209 timing.heartbeat
210 },
211 start_pending: changing,
212 report_pending: changing,
213 commit_pending: false,
214 activation_pending: VoterSet::EMPTY,
215 exit_votes: VoterSet::EMPTY,
216 exit_heard_at: Voters::splat(Duration::ZERO),
217 exit_retry_at: None,
218 exit_echo: None,
219 exit_echo_at: Voters::splat(Duration::ZERO),
220 fence_answers: VoterSet::EMPTY,
221 stale_answers: VoterSet::EMPTY,
222 })
223 }
224
225 pub fn normal(&self) -> Option<&NormalReplica> {
227 match &self.role {
228 Role::Normal(normal) => Some(normal),
229 Role::ViewChanging(_) | Role::Installing(_) | Role::Fenced(_) => None,
230 }
231 }
232
233 pub fn election_needs_sync(&self) -> bool {
235 let Role::ViewChanging(changing) = &self.role else {
236 return false;
237 };
238 let journal = changing.normal_snapshot().journal;
239 journal.written == journal.accepted && journal.written > journal.durable
240 }
241
242 pub fn scope(&self) -> Scope {
244 match &self.role {
245 Role::Normal(normal) => normal.scope(),
246 Role::ViewChanging(changing) => changing.scope(),
247 Role::Installing(installing) => installing.requested_scope(),
248 Role::Fenced(scope) => *scope,
249 }
250 }
251
252 pub fn into_role(self) -> Role {
255 self.role
256 }
257
258 pub fn select(
261 &mut self,
262 lookup: impl FnMut(LogSource, Prefix) -> Option<Digest>,
263 ) -> Result<SelectedView, DriverError> {
264 Ok(self.changing()?.select(lookup)?)
265 }
266
267 pub fn history_sources(&self) -> [Option<LogSource>; 3] {
269 match &self.role {
270 Role::ViewChanging(changing) => changing.history_sources(),
271 Role::Normal(_) | Role::Installing(_) | Role::Fenced(_) => [None; 3],
272 }
273 }
274
275 pub fn lookup_ops(&self) -> [Option<OpNumber>; 8] {
278 match &self.role {
279 Role::ViewChanging(changing) => changing.lookup_ops(),
280 Role::Normal(_) | Role::Installing(_) | Role::Fenced(_) => [None; 8],
281 }
282 }
283
284 fn changing(&mut self) -> Result<&mut ViewChange, DriverError> {
285 match &mut self.role {
286 Role::ViewChanging(changing) => Ok(changing),
287 Role::Normal(_) | Role::Installing(_) => Err(ReplicationError::WrongRole.into()),
288 Role::Fenced(_) => Err(ViewChangeError::Faulted.into()),
289 }
290 }
291
292 fn require_normal(&self) -> Result<&NormalReplica, DriverError> {
293 Ok(self.normal().ok_or(ReplicationError::NotNormal)?)
294 }
295
296 fn require_healthy(&self) -> Result<(), DriverError> {
298 match &self.role {
299 Role::Normal(normal) if normal.snapshot().status == Status::Faulted => {
300 Err(ViewChangeError::Faulted.into())
301 }
302 Role::ViewChanging(changing)
303 if changing.normal_snapshot().status == Status::Faulted =>
304 {
305 Err(ViewChangeError::Faulted.into())
306 }
307 Role::Installing(installing) => Ok(installing.require_pending()?),
308 Role::Fenced(_) => Err(ViewChangeError::Faulted.into()),
309 Role::Normal(_) | Role::ViewChanging(_) => Ok(()),
310 }
311 }
312
313 fn take_role(&mut self) -> Role {
316 let fenced = Role::Fenced(self.scope());
317 std::mem::replace(&mut self.role, fenced)
318 }
319
320 fn observe_time(&mut self, now: Duration) -> Result<(), DriverError> {
321 if now < self.now {
322 return Err(DriverError::ClockRegressed);
323 }
324 self.timing.validate(now)?;
325 self.now = now;
326 Ok(())
327 }
328
329 fn progress_timeout(&self) -> Duration {
330 if self
331 .normal()
332 .is_some_and(|normal| normal.snapshot().ready_for_appends)
333 {
334 self.timing.primary_timeout
335 } else {
336 self.election_delay
337 }
338 }
339
340 fn observe_normal_progress(
341 &mut self,
342 previous: &ReplicaSnapshot,
343 current: &ReplicaSnapshot,
344 now: Duration,
345 ) {
346 if current.committed != previous.committed {
347 self.progress_deadline = now.saturating_add(self.progress_timeout());
348 }
349 if current.journal.durable > previous.journal.durable {
350 self.persistence_deadline = now.saturating_add(self.timing.primary_timeout);
351 }
352 }
353
354 fn normal_timed_out(&self, snapshot: &ReplicaSnapshot) -> bool {
355 let primary = VoterIndex::primary(snapshot.scope.view) == self.local;
356 let outstanding = snapshot.accepted != snapshot.committed || !snapshot.ready_for_appends;
357 (!primary && self.now >= self.contact_deadline)
358 || (outstanding && self.now >= self.progress_deadline)
359 || (snapshot.accepted.op > snapshot.journal.durable
360 && self.now >= self.persistence_deadline)
361 || self
362 .storage_busy_since
363 .is_some_and(|since| self.now >= since.saturating_add(self.timing.primary_timeout))
364 }
365
366 fn next_view(&mut self) -> Result<(), DriverError> {
367 let view = self
368 .scope()
369 .view
370 .checked_add(1)
371 .ok_or(ViewChangeError::ViewExhausted)?;
372 self.change_view(view)
373 }
374
375 fn change_view(&mut self, view: u64) -> Result<(), DriverError> {
376 match &mut self.role {
377 Role::ViewChanging(changing) => changing.advance_view(view)?,
378 Role::Installing(installing) => installing.request_view(view)?,
379 Role::Normal(normal) if normal.snapshot().status == Status::Faulted => {
380 return Err(ViewChangeError::Faulted.into());
381 }
382 Role::Normal(_) => {
383 let Role::Normal(normal) = self.take_role() else {
384 return Err(ViewChangeError::Faulted.into());
385 };
386 self.role = Role::ViewChanging(normal.into_view_change(view)?);
387 }
388 Role::Fenced(_) => return Err(ViewChangeError::Faulted.into()),
389 }
390 self.exit_votes = VoterSet::EMPTY;
391 self.exit_retry_at = None;
392 self.fence_answers = VoterSet::EMPTY;
395 self.stale_answers = VoterSet::EMPTY;
396 self.election_deadline = self.now.saturating_add(self.election_delay);
397 self.retry_at = self.now.saturating_add(self.timing.retransmit);
398 self.start_pending = true;
399 self.report_pending = true;
400 self.commit_pending = false;
401 self.activation_pending = VoterSet::EMPTY;
402 Ok(())
403 }
404}
405
406#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
408pub enum DriverError {
409 #[error("invalid replica timing intervals")]
411 InvalidTiming,
412 #[error("replica clock regressed")]
414 ClockRegressed,
415 #[error("replica clock exhausted")]
417 TimeExhausted,
418 #[error("stale replica application validation")]
420 StaleValidation,
421 #[error(transparent)]
423 Replication(#[from] ReplicationError),
424 #[error(transparent)]
426 ViewChange(#[from] ViewChangeError),
427}