use ozzy_proto::NodeId;
use crate::{
Configuration, Digest, JournalGeneration, NormalReplica, OpNumber, PipelineLimits, Prefix,
ReplicaSnapshot, ReplicationError, Scope, Status, SyncTicket, WriteTicket,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FrozenLog {
pub last_normal_view: u64,
pub accepted: Prefix,
pub committed: Prefix,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RecoveredState {
pub scope: Scope,
pub log: FrozenLog,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PromiseTicket {
scope: Scope,
generation: JournalGeneration,
log: FrozenLog,
}
impl PromiseTicket {
pub const fn scope(self) -> Scope {
self.scope
}
pub const fn generation(self) -> JournalGeneration {
self.generation
}
pub const fn log(self) -> FrozenLog {
self.log
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StartViewChange {
pub scope: Scope,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DoViewChange {
pub scope: Scope,
pub generation: JournalGeneration,
pub log: FrozenLog,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LogSource {
pub voter: NodeId,
pub generation: JournalGeneration,
pub accepted: Prefix,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SelectedView {
scope: Scope,
source: LogSource,
committed: Prefix,
voters: u8,
}
impl SelectedView {
pub const fn scope(self) -> Scope {
self.scope
}
pub const fn source(self) -> LogSource {
self.source
}
pub const fn committed(self) -> Prefix {
self.committed
}
pub const fn voter_mask(self) -> u8 {
self.voters
}
}
#[derive(Debug)]
pub struct ViewChange {
pub(crate) normal: NormalReplica,
pub(crate) configuration: Configuration,
pub(crate) local: usize,
view: u64,
promised: u64,
pending: Option<PromiseTicket>,
pub(crate) completed: Option<PromiseTicket>,
starts: u8,
reports: [Option<DoViewChange>; 3],
pub(crate) selected: Option<SelectedView>,
}
impl ViewChange {
pub(crate) fn after_abandon(
mut normal: NormalReplica,
previous: PromiseTicket,
requested_view: u64,
) -> Result<Self, ViewChangeError> {
normal.restore_fenced(previous.generation(), previous.log());
let mut changing = normal.into_view_change(requested_view)?;
changing.promised = previous.scope().view;
changing.completed = Some(previous);
Ok(changing)
}
pub fn recover_intact(
configuration: Configuration,
local: NodeId,
generation: JournalGeneration,
recovered: RecoveredState,
limits: PipelineLimits,
) -> Result<Self, ViewChangeError> {
if configuration.policy() == crate::QuorumPolicy::Replicated {
return Err(ViewChangeError::VolatileRestart);
}
Self::recover_verified(configuration, local, generation, recovered, limits)
}
pub fn recover_drained(
configuration: Configuration,
local: NodeId,
generation: JournalGeneration,
recovered: RecoveredState,
limits: PipelineLimits,
) -> Result<Self, ViewChangeError> {
if configuration.policy() != crate::QuorumPolicy::Replicated {
return Err(ReplicationError::PolicyMismatch.into());
}
Self::recover_verified(configuration, local, generation, recovered, limits)
}
fn recover_verified(
configuration: Configuration,
local: NodeId,
generation: JournalGeneration,
recovered: RecoveredState,
limits: PipelineLimits,
) -> Result<Self, ViewChangeError> {
if recovered.scope
!= (Scope {
view: recovered.scope.view,
..configuration.scope()
})
{
return Err(ReplicationError::ScopeMismatch.into());
}
if recovered.log.last_normal_view > recovered.scope.view {
return Err(ViewChangeError::InvalidReport);
}
let view = if recovered.scope.view > recovered.log.last_normal_view {
recovered.scope.view
} else {
recovered
.scope
.view
.checked_add(1)
.ok_or(ViewChangeError::ViewExhausted)?
};
validate_report(DoViewChange {
scope: Scope {
view,
..recovered.scope
},
generation,
log: recovered.log,
})?;
let normal =
NormalReplica::recover_fenced(configuration, local, generation, recovered.log, limits)?;
let mut changing = normal.into_view_change(view)?;
changing.promised = recovered.scope.view;
if changing.promised == view {
changing.starts |= 1 << changing.local; changing.completed = Some(PromiseTicket {
scope: changing.scope(),
generation,
log: recovered.log,
});
}
Ok(changing)
}
pub(crate) fn begin(
mut normal: NormalReplica,
configuration: Configuration,
local: usize,
view: u64,
) -> Result<Self, ViewChangeError> {
normal.fence();
if normal.snapshot().status == Status::Faulted {
return Err(ViewChangeError::Faulted);
}
if view <= configuration.scope().view {
return Err(ViewChangeError::ViewNotHigher);
}
Ok(Self {
normal,
configuration,
local,
view,
promised: configuration.scope().view,
pending: None,
completed: None,
starts: 0,
reports: [None; 3],
selected: None,
})
}
pub fn normal_snapshot(&self) -> ReplicaSnapshot {
self.normal.snapshot()
}
pub fn scope(&self) -> Scope {
Scope {
view: self.view,
..self.configuration.scope()
}
}
pub const fn promised_view(&self) -> u64 {
self.promised
}
pub fn advance_view(&mut self, view: u64) -> Result<(), ViewChangeError> {
self.require_active()?;
if view <= self.view {
return Err(ViewChangeError::ViewNotHigher);
}
self.view = view;
self.starts = 0;
self.reports = [None; 3];
self.selected = None;
Ok(())
}
pub fn complete_write(&mut self, ticket: WriteTicket) -> Result<(), ViewChangeError> {
Ok(self.normal.complete_write(ticket)?)
}
pub fn begin_sync(&self) -> Result<SyncTicket, ViewChangeError> {
Ok(self.normal.begin_sync()?)
}
pub fn complete_sync(&mut self, ticket: SyncTicket) -> Result<(), ViewChangeError> {
Ok(self.normal.complete_sync(ticket)?)
}
pub fn fail_io(&mut self, generation: JournalGeneration) -> Result<(), ViewChangeError> {
Ok(self.normal.fail_io(generation)?)
}
pub fn begin_promise(&mut self) -> Result<PromiseTicket, ViewChangeError> {
self.require_active()?;
if self.pending.is_some() {
return Err(ViewChangeError::PromisePending);
}
if self.promised == self.view {
return Err(ViewChangeError::AlreadyPromised);
}
let snapshot = self.normal.snapshot();
if snapshot.journal.durable != snapshot.accepted.op {
return Err(ViewChangeError::StoragePending);
}
let ticket = PromiseTicket {
scope: self.scope(),
generation: snapshot.journal.generation,
log: FrozenLog {
last_normal_view: snapshot.scope.view,
accepted: snapshot.accepted,
committed: snapshot.committed,
},
};
self.pending = Some(ticket);
Ok(ticket)
}
pub fn complete_promise(&mut self, ticket: PromiseTicket) -> Result<(), ViewChangeError> {
self.require_active()?;
if self.completed == Some(ticket) {
return Ok(());
}
self.require_ticket(ticket)?;
self.pending = None;
self.completed = Some(ticket);
self.promised = ticket.scope.view;
if self.promised == self.view {
self.starts |= 1 << self.local;
}
Ok(())
}
pub fn fail_promise(&mut self, ticket: PromiseTicket) -> Result<(), ViewChangeError> {
self.require_active()?;
self.require_ticket(ticket)?;
self.fail_io(ticket.generation)
}
pub fn start_message(&self) -> Result<StartViewChange, ViewChangeError> {
self.require_promised()?;
Ok(StartViewChange {
scope: self.scope(),
})
}
pub fn receive_start(
&mut self,
from: NodeId,
message: StartViewChange,
) -> Result<(), ViewChangeError> {
self.require_scope(message.scope)?;
let voter = self.configuration.voter_index(from)?;
if voter == self.local {
return Err(ReplicationError::WrongRole.into());
}
self.starts |= 1 << voter;
Ok(())
}
pub fn report(&mut self) -> Result<DoViewChange, ViewChangeError> {
self.require_promised()?;
if self.starts.count_ones() < 2 {
return Err(ViewChangeError::StartQuorumMissing);
}
let promise = self.completed.expect("requested view promised");
let report = DoViewChange {
scope: self.scope(),
generation: promise.generation,
log: promise.log,
};
self.reports[self.local] = Some(report);
Ok(report)
}
pub fn receive_report(
&mut self,
from: NodeId,
report: DoViewChange,
) -> Result<(), ViewChangeError> {
self.require_scope(report.scope)?;
self.require_primary()?;
let voter = self.configuration.voter_index(from)?;
if voter == self.local {
return Err(ReplicationError::WrongRole.into());
}
validate_report(report)?;
if let Some(previous) = self.reports[voter] {
if previous.log != report.log {
return Err(self.conflict());
}
return Ok(());
}
if self.selected.is_some() {
return Err(ViewChangeError::SelectionFrozen);
}
self.reports[voter] = Some(report);
Ok(())
}
pub fn select(
&mut self,
mut lookup: impl FnMut(LogSource, OpNumber) -> Option<Digest>,
) -> Result<SelectedView, ViewChangeError> {
self.require_promised()?;
self.require_primary()?;
if let Some(selected) = self.selected {
return Ok(selected);
}
let reports = self.reports;
if reports[self.local].is_none() || reports.iter().flatten().count() < 2 {
return Err(ViewChangeError::ReportQuorumMissing);
}
let mut chosen = self.local;
let mut voters = 0;
for (index, report) in reports.iter().enumerate() {
let Some(report) = report else {
continue;
};
voters |= 1 << index;
let current = reports[chosen].expect("selected report");
let rank = (report.log.last_normal_view, report.log.accepted.op);
let best = (current.log.last_normal_view, current.log.accepted.op);
if rank == best && report.log.accepted != current.log.accepted {
return Err(self.conflict());
}
if rank > best {
chosen = index;
}
}
let report = reports[chosen].expect("selected report");
let source = LogSource {
voter: self.configuration.voters()[chosen],
generation: report.generation,
accepted: report.log.accepted,
};
let mut committed = Prefix::GENESIS;
for candidate in reports.iter().flatten() {
if candidate.log.committed.op > committed.op {
committed = candidate.log.committed;
}
self.verify_prefix(source, candidate.log.committed, &mut lookup)?;
if candidate.log.last_normal_view == report.log.last_normal_view {
self.verify_prefix(source, candidate.log.accepted, &mut lookup)?;
}
}
let selected = SelectedView {
scope: self.scope(),
source,
committed,
voters,
};
self.selected = Some(selected);
Ok(selected)
}
pub(crate) fn verify_prefix(
&mut self,
source: LogSource,
prefix: Prefix,
lookup: &mut impl FnMut(LogSource, OpNumber) -> Option<Digest>,
) -> Result<(), ViewChangeError> {
if prefix.op > source.accepted.op {
return Err(self.conflict());
}
let digest = if prefix.op == OpNumber(0) {
Digest::ZERO
} else if prefix.op == source.accepted.op {
source.accepted.digest
} else {
lookup(source, prefix.op).ok_or(ViewChangeError::HistoryMissing)?
};
if digest != prefix.digest {
return Err(self.conflict());
}
Ok(())
}
fn require_active(&self) -> Result<(), ViewChangeError> {
if self.normal.snapshot().status == Status::Faulted {
return Err(ViewChangeError::Faulted);
}
Ok(())
}
pub(crate) fn require_scope(&self, scope: Scope) -> Result<(), ViewChangeError> {
self.require_active()?;
if scope != self.scope() {
return Err(ReplicationError::ScopeMismatch.into());
}
Ok(())
}
pub(crate) fn require_primary(&self) -> Result<(), ViewChangeError> {
if self.configuration.primary(self.view) != self.configuration.voters()[self.local] {
return Err(ReplicationError::WrongRole.into());
}
Ok(())
}
pub(crate) fn require_promised(&self) -> Result<(), ViewChangeError> {
self.require_active()?;
if self.promised != self.view {
return Err(ViewChangeError::PromiseRequired);
}
Ok(())
}
fn require_ticket(&self, ticket: PromiseTicket) -> Result<(), ViewChangeError> {
if self.pending != Some(ticket) {
return Err(ViewChangeError::StalePromise);
}
Ok(())
}
fn conflict(&mut self) -> ViewChangeError {
self.normal
.fail_io(self.normal.snapshot().journal.generation)
.expect("current generation");
ViewChangeError::ConflictingHistory
}
}
pub(crate) fn valid_prefix(prefix: Prefix) -> bool {
prefix.op.0 != u64::MAX && ((prefix.op.0 == 0) == (prefix.digest == Digest::ZERO))
}
fn validate_report(report: DoViewChange) -> Result<(), ViewChangeError> {
let log = report.log;
if log.last_normal_view >= report.scope.view
|| !valid_prefix(log.accepted)
|| !valid_prefix(log.committed)
|| log.committed.op > log.accepted.op
|| (log.committed.op == log.accepted.op && log.committed != log.accepted)
{
return Err(ViewChangeError::InvalidReport);
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum ViewChangeError {
#[error("retained-memory voter restart requires fresh recovery or drained-state evidence")]
VolatileRestart,
#[error(transparent)]
Replication(#[from] ReplicationError),
#[error("view must increase")]
ViewNotHigher,
#[error("durable view number exhausted")]
ViewExhausted,
#[error("old-view journal work has not settled")]
StoragePending,
#[error("a view promise is already pending")]
PromisePending,
#[error("requested view already promised")]
AlreadyPromised,
#[error("stale or foreign view-promise completion")]
StalePromise,
#[error("requested view promise has not completed")]
PromiseRequired,
#[error("start-view-change quorum missing")]
StartQuorumMissing,
#[error("do-view-change quorum missing")]
ReportQuorumMissing,
#[error("selected lineage evidence missing")]
HistoryMissing,
#[error("invalid frozen view-change report")]
InvalidReport,
#[error("conflicting view-change history")]
ConflictingHistory,
#[error("view selection already frozen")]
SelectionFrozen,
#[error("view has not been selected")]
SelectionRequired,
#[error("installation reused the old writer generation")]
ReusedGeneration,
#[error("stale or foreign installation completion")]
StaleInstallation,
#[error("installation already completed")]
InstallationCompleted,
#[error("installed application state is not at the required committed prefix")]
ApplicationPending,
#[error("view change faulted")]
Faulted,
}