mod recovery;
mod selection;
use ozzy_proto::NodeId;
use crate::{
Configuration, Digest, JournalGeneration, NormalReplica, Prefix, ReplicaSnapshot,
ReplicationError, Scope, Status, SyncTicket, VoterIndex, VoterSet, Voters, 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: VoterSet,
}
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.mask()
}
}
#[derive(Debug)]
pub struct ViewChange {
pub(crate) normal: NormalReplica,
pub(crate) configuration: Configuration,
pub(crate) local: VoterIndex,
view: u64,
promise: Promise,
pending: Option<PromiseTicket>,
starts: VoterSet,
reports: Voters<Option<DoViewChange>>,
pub(crate) selected: Option<SelectedView>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Promise {
Prior(u64),
Completed(PromiseTicket),
}
impl Promise {
const fn view(self) -> u64 {
match self {
Self::Prior(view) => view,
Self::Completed(ticket) => ticket.scope.view,
}
}
}
impl ViewChange {
pub fn history_sources(&self) -> [Option<LogSource>; 3] {
VoterIndex::ALL.map(|voter| {
self.reports[voter].map(|report| LogSource {
voter: self.configuration.voter(voter),
generation: report.generation,
accepted: report.log.accepted,
})
})
}
pub(crate) fn begin(
mut normal: NormalReplica,
configuration: Configuration,
local: VoterIndex,
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,
promise: Promise::Prior(configuration.scope().view),
pending: None,
starts: VoterSet::EMPTY,
reports: Voters::splat(None),
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.promise.view()
}
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 = VoterSet::EMPTY;
self.reports = Voters::splat(None);
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.promise.view() == 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.promise == Promise::Completed(ticket) {
return Ok(());
}
self.require_ticket(ticket)?;
self.pending = None;
self.promise = Promise::Completed(ticket);
if ticket.scope.view == self.view {
self.starts.insert(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.insert(voter);
Ok(())
}
pub fn report(&mut self) -> Result<DoViewChange, ViewChangeError> {
let promise = self.require_promised()?;
if self.starts.len() < 2 {
return Err(ViewChangeError::StartQuorumMissing);
}
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(())
}
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 VoterIndex::primary(self.view) != self.local {
return Err(ReplicationError::WrongRole.into());
}
Ok(())
}
pub(crate) fn require_promised(&self) -> Result<PromiseTicket, ViewChangeError> {
self.require_active()?;
match self.promise {
Promise::Completed(ticket) if ticket.scope.view == self.view => Ok(ticket),
Promise::Prior(_) | Promise::Completed(_) => Err(ViewChangeError::PromiseRequired),
}
}
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.fault();
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(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,
}