mod deadline;
mod exit_view;
mod installation;
mod poll;
mod receive;
mod storage;
mod validation;
pub use installation::ActivationTicket;
pub use validation::ValidationTicket;
use std::time::Duration;
use ozzy_proto::NodeId;
use crate::wire::Control;
use crate::{
Configuration, Digest, InstallingView, LogSource, NormalReplica, OpNumber, Prefix,
PromiseTicket, ReplicaSnapshot, ReplicationError, Scope, SelectedView, Status, ViewChange,
ViewChangeError, VoterIndex, VoterSet, Voters,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Timing {
pub heartbeat: Duration,
pub primary_timeout: Duration,
pub retransmit: Duration,
pub election_timeout: Duration,
pub max_election_timeout: Duration,
}
impl Timing {
fn validate(self, now: Duration) -> Result<(), DriverError> {
if self.heartbeat.is_zero()
|| self.retransmit.is_zero()
|| self.heartbeat >= self.primary_timeout
|| self.retransmit >= self.election_timeout
|| self.election_timeout > self.max_election_timeout
{
return Err(DriverError::InvalidTiming);
}
if now
.checked_add(self.max_election_timeout.max(self.primary_timeout))
.is_none()
{
return Err(DriverError::TimeExhausted);
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Action {
Broadcast(Control),
Send {
to: NodeId,
message: Control,
},
PersistPromise(PromiseTicket),
}
#[derive(Debug)]
#[expect(
clippy::large_enum_variant,
reason = "move preallocated roles without boxing"
)]
pub enum Role {
Normal(NormalReplica),
ViewChanging(ViewChange),
Installing(InstallingView),
Fenced(Scope),
}
#[derive(Debug)]
pub struct ReplicaDriver {
role: Role,
configuration: Configuration,
local: VoterIndex,
timing: Timing,
now: Duration,
contact_deadline: Duration,
progress_deadline: Duration,
persistence_deadline: Duration,
storage_busy_since: Option<Duration>,
election_deadline: Duration,
election_delay: Duration,
retry_at: Duration,
start_pending: bool,
report_pending: bool,
commit_pending: bool,
activation_pending: VoterSet,
exit_votes: VoterSet,
exit_heard_at: Voters<Duration>,
exit_retry_at: Option<Duration>,
exit_echo: Option<(Scope, VoterSet)>,
exit_echo_at: Voters<Duration>,
}
impl ReplicaDriver {
pub fn from_normal(
normal: NormalReplica,
now: Duration,
timing: Timing,
) -> Result<Self, DriverError> {
let (configuration, local) = normal.driver_identity();
Self::new(Role::Normal(normal), configuration, local, now, timing)
}
pub fn from_view_change(
changing: ViewChange,
now: Duration,
timing: Timing,
) -> Result<Self, DriverError> {
let configuration = changing.configuration;
let local = changing.local;
Self::new(
Role::ViewChanging(changing),
configuration,
local,
now,
timing,
)
}
fn new(
role: Role,
configuration: Configuration,
local: VoterIndex,
now: Duration,
timing: Timing,
) -> Result<Self, DriverError> {
timing.validate(now)?;
let changing = matches!(role, Role::ViewChanging(_));
let activating =
matches!(&role, Role::Normal(normal) if !normal.snapshot().ready_for_appends);
Ok(Self {
role,
configuration,
local,
timing,
now,
contact_deadline: now.saturating_add(timing.primary_timeout),
persistence_deadline: now.saturating_add(timing.primary_timeout),
storage_busy_since: None,
progress_deadline: now
+ if activating {
timing.election_timeout
} else {
timing.primary_timeout
},
election_deadline: now.saturating_add(timing.election_timeout),
election_delay: timing.election_timeout,
retry_at: now
+ if changing {
timing.retransmit
} else {
timing.heartbeat
},
start_pending: changing,
report_pending: changing,
commit_pending: false,
activation_pending: VoterSet::EMPTY,
exit_votes: VoterSet::EMPTY,
exit_heard_at: Voters::splat(Duration::ZERO),
exit_retry_at: None,
exit_echo: None,
exit_echo_at: Voters::splat(Duration::ZERO),
})
}
pub fn normal(&self) -> Option<&NormalReplica> {
match &self.role {
Role::Normal(normal) => Some(normal),
Role::ViewChanging(_) | Role::Installing(_) | Role::Fenced(_) => None,
}
}
pub fn election_needs_sync(&self) -> bool {
let Role::ViewChanging(changing) = &self.role else {
return false;
};
let journal = changing.normal_snapshot().journal;
journal.written == journal.accepted && journal.written > journal.durable
}
pub fn scope(&self) -> Scope {
match &self.role {
Role::Normal(normal) => normal.scope(),
Role::ViewChanging(changing) => changing.scope(),
Role::Installing(installing) => installing.requested_scope(),
Role::Fenced(scope) => *scope,
}
}
pub fn into_role(self) -> Role {
self.role
}
pub fn select(
&mut self,
lookup: impl FnMut(LogSource, Prefix) -> Option<Digest>,
) -> Result<SelectedView, DriverError> {
Ok(self.changing()?.select(lookup)?)
}
pub fn history_sources(&self) -> [Option<LogSource>; 3] {
match &self.role {
Role::ViewChanging(changing) => changing.history_sources(),
Role::Normal(_) | Role::Installing(_) | Role::Fenced(_) => [None; 3],
}
}
pub fn lookup_ops(&self) -> [Option<OpNumber>; 8] {
match &self.role {
Role::ViewChanging(changing) => changing.lookup_ops(),
Role::Normal(_) | Role::Installing(_) | Role::Fenced(_) => [None; 8],
}
}
fn changing(&mut self) -> Result<&mut ViewChange, DriverError> {
match &mut self.role {
Role::ViewChanging(changing) => Ok(changing),
Role::Normal(_) | Role::Installing(_) => Err(ReplicationError::WrongRole.into()),
Role::Fenced(_) => Err(ViewChangeError::Faulted.into()),
}
}
fn require_normal(&self) -> Result<&NormalReplica, DriverError> {
Ok(self.normal().ok_or(ReplicationError::NotNormal)?)
}
fn require_healthy(&self) -> Result<(), DriverError> {
match &self.role {
Role::Normal(normal) if normal.snapshot().status == Status::Faulted => {
Err(ViewChangeError::Faulted.into())
}
Role::ViewChanging(changing)
if changing.normal_snapshot().status == Status::Faulted =>
{
Err(ViewChangeError::Faulted.into())
}
Role::Installing(installing) => Ok(installing.require_pending()?),
Role::Fenced(_) => Err(ViewChangeError::Faulted.into()),
Role::Normal(_) | Role::ViewChanging(_) => Ok(()),
}
}
fn take_role(&mut self) -> Role {
let fenced = Role::Fenced(self.scope());
std::mem::replace(&mut self.role, fenced)
}
fn observe_time(&mut self, now: Duration) -> Result<(), DriverError> {
if now < self.now {
return Err(DriverError::ClockRegressed);
}
self.timing.validate(now)?;
self.now = now;
Ok(())
}
fn progress_timeout(&self) -> Duration {
if self
.normal()
.is_some_and(|normal| normal.snapshot().ready_for_appends)
{
self.timing.primary_timeout
} else {
self.election_delay
}
}
fn observe_normal_progress(
&mut self,
previous: &ReplicaSnapshot,
current: &ReplicaSnapshot,
now: Duration,
) {
if current.committed != previous.committed {
self.progress_deadline = now.saturating_add(self.progress_timeout());
}
if current.journal.durable > previous.journal.durable {
self.persistence_deadline = now.saturating_add(self.timing.primary_timeout);
}
}
fn normal_timed_out(&self, snapshot: &ReplicaSnapshot) -> bool {
let primary = VoterIndex::primary(snapshot.scope.view) == self.local;
let outstanding = snapshot.accepted != snapshot.committed || !snapshot.ready_for_appends;
(!primary && self.now >= self.contact_deadline)
|| (outstanding && self.now >= self.progress_deadline)
|| (snapshot.accepted.op > snapshot.journal.durable
&& self.now >= self.persistence_deadline)
|| self
.storage_busy_since
.is_some_and(|since| self.now >= since.saturating_add(self.timing.primary_timeout))
}
fn next_view(&mut self) -> Result<(), DriverError> {
let view = self
.scope()
.view
.checked_add(1)
.ok_or(ViewChangeError::ViewExhausted)?;
self.change_view(view)
}
fn change_view(&mut self, view: u64) -> Result<(), DriverError> {
match &mut self.role {
Role::ViewChanging(changing) => changing.advance_view(view)?,
Role::Installing(installing) => installing.request_view(view)?,
Role::Normal(normal) if normal.snapshot().status == Status::Faulted => {
return Err(ViewChangeError::Faulted.into());
}
Role::Normal(_) => {
let Role::Normal(normal) = self.take_role() else {
return Err(ViewChangeError::Faulted.into());
};
self.role = Role::ViewChanging(normal.into_view_change(view)?);
}
Role::Fenced(_) => return Err(ViewChangeError::Faulted.into()),
}
self.exit_votes = VoterSet::EMPTY;
self.exit_retry_at = None;
self.election_deadline = self.now.saturating_add(self.election_delay);
self.retry_at = self.now.saturating_add(self.timing.retransmit);
self.start_pending = true;
self.report_pending = true;
self.commit_pending = false;
self.activation_pending = VoterSet::EMPTY;
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum DriverError {
#[error("invalid replica timing intervals")]
InvalidTiming,
#[error("replica clock regressed")]
ClockRegressed,
#[error("replica clock exhausted")]
TimeExhausted,
#[error("stale replica application validation")]
StaleValidation,
#[error(transparent)]
Replication(#[from] ReplicationError),
#[error(transparent)]
ViewChange(#[from] ViewChangeError),
}