use ozzy_proto::NodeId;
use crate::{
Configuration, Digest, JournalGeneration, LogSource, NormalReplica, OpNumber, Prefix,
PreparedOperation, PromiseTicket, ReplicationError, Scope, ViewChange, ViewChangeError,
VoterIndex,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StartView {
pub scope: Scope,
pub generation: JournalGeneration,
pub accepted: Prefix,
pub committed: Prefix,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReceivedStart {
pub from: NodeId,
pub start: StartView,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InstallTicket {
scope: Scope,
previous_generation: JournalGeneration,
generation: JournalGeneration,
source: LogSource,
protected_committed: Prefix,
committed: Prefix,
}
impl InstallTicket {
pub const fn scope(self) -> Scope {
self.scope
}
pub const fn previous_generation(self) -> JournalGeneration {
self.previous_generation
}
pub const fn generation(self) -> JournalGeneration {
self.generation
}
pub const fn source(self) -> LogSource {
self.source
}
pub const fn protected_committed(self) -> Prefix {
self.protected_committed
}
pub const fn accepted(self) -> Prefix {
self.source.accepted
}
pub const fn committed(self) -> Prefix {
self.committed
}
}
#[derive(Debug)]
#[expect(
clippy::large_enum_variant,
reason = "rare ownership transition moves preallocated state without boxing"
)]
pub enum InstallOutcome {
Normal(NormalReplica),
ViewChanging(ViewChange),
}
#[derive(Debug)]
pub struct RejectedInstallation {
view_change: ViewChange,
error: ViewChangeError,
}
impl RejectedInstallation {
pub const fn error(&self) -> ViewChangeError {
self.error
}
pub fn into_view_change(self) -> ViewChange {
self.view_change
}
}
impl std::fmt::Display for RejectedInstallation {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.error, formatter)
}
}
impl std::error::Error for RejectedInstallation {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.error)
}
}
#[derive(Debug)]
pub struct InstallingView {
ticket: InstallTicket,
previous_promise: PromiseTicket,
state: InstallState,
configuration: Configuration,
requested_view: u64,
validated_through: Prefix,
}
#[derive(Debug)]
#[expect(
clippy::large_enum_variant,
reason = "the staged role moves without boxing"
)]
enum InstallState {
Pending(NormalReplica),
Failed,
Completed,
}
impl InstallState {
fn pending(&self) -> Result<&NormalReplica, ViewChangeError> {
match self {
Self::Pending(normal) => Ok(normal),
Self::Failed => Err(ViewChangeError::Faulted),
Self::Completed => Err(ViewChangeError::InstallationCompleted),
}
}
fn take(&mut self) -> Result<NormalReplica, ViewChangeError> {
match std::mem::replace(self, Self::Completed) {
Self::Pending(normal) => Ok(normal),
Self::Failed => {
*self = Self::Failed;
Err(ViewChangeError::Faulted)
}
Self::Completed => Err(ViewChangeError::InstallationCompleted),
}
}
}
impl InstallingView {
pub(crate) fn requested_scope(&self) -> Scope {
Scope {
view: self.requested_view,
..self.ticket.scope()
}
}
pub const fn ticket(&self) -> InstallTicket {
self.ticket
}
pub fn validate_suffix(
&mut self,
operations: &[PreparedOperation],
) -> Result<(), ViewChangeError> {
let (through, _) = self.state.pending()?.validate_install_chunk(
self.ticket,
self.validated_through,
operations,
)?;
self.validated_through = through;
Ok(())
}
pub fn observe_view(&mut self, from: NodeId, scope: Scope) -> Result<(), ViewChangeError> {
self.require_pending()?;
self.configuration.voter_index(from)?;
if (Scope {
view: scope.view,
..self.ticket.scope
}) != scope
{
return Err(ReplicationError::ScopeMismatch.into());
}
self.requested_view = self.requested_view.max(scope.view);
Ok(())
}
pub fn request_view(&mut self, view: u64) -> Result<(), ViewChangeError> {
self.require_pending()?;
if view <= self.requested_view {
return Err(ViewChangeError::ViewNotHigher);
}
self.requested_view = view;
Ok(())
}
pub fn complete(
&mut self,
ticket: InstallTicket,
applied: Prefix,
) -> Result<InstallOutcome, ViewChangeError> {
self.require_ticket(ticket)?;
if self.validated_through != ticket.accepted() {
return Err(ViewChangeError::HistoryMissing);
}
if applied != ticket.committed {
return Err(ViewChangeError::ApplicationPending);
}
let mut normal = self.state.take()?;
normal.activate_installed();
if self.requested_view > ticket.scope.view {
Ok(InstallOutcome::ViewChanging(
normal.into_view_change(self.requested_view)?,
))
} else {
Ok(InstallOutcome::Normal(normal))
}
}
pub fn complete_abandon(
&mut self,
ticket: InstallTicket,
) -> Result<ViewChange, ViewChangeError> {
self.require_ticket(ticket)?;
if self.requested_view <= ticket.scope.view {
return Err(ViewChangeError::ViewNotHigher);
}
ViewChange::after_abandon(
self.state.take()?,
self.previous_promise,
self.requested_view,
)
}
pub fn fail(&mut self, ticket: InstallTicket) -> Result<(), ViewChangeError> {
self.require_ticket(ticket)?;
self.state = InstallState::Failed;
Ok(())
}
pub(crate) fn require_pending(&self) -> Result<(), ViewChangeError> {
self.state.pending().map(|_| ())
}
fn require_ticket(&self, ticket: InstallTicket) -> Result<(), ViewChangeError> {
self.require_pending()?;
if ticket != self.ticket {
return Err(ViewChangeError::StaleInstallation);
}
Ok(())
}
}
#[derive(Debug, Clone, Copy)]
struct Admitted {
ticket: InstallTicket,
start: StartView,
promise: PromiseTicket,
body_bytes: usize,
}
#[derive(Debug, Clone, Copy)]
enum Suffix<'a> {
Supplied(&'a [PreparedOperation]),
Streamed,
}
impl ViewChange {
#[expect(
clippy::result_large_err,
reason = "rejection returns owned fenced role"
)]
pub fn begin_primary_install(
self,
generation: JournalGeneration,
) -> Result<InstallingView, RejectedInstallation> {
let admitted = self.admit_primary(generation);
self.stage(&admitted, Suffix::Streamed)
}
#[expect(
clippy::result_large_err,
reason = "rejection returns owned fenced role"
)]
pub fn begin_backup_install(
mut self,
received: ReceivedStart,
generation: JournalGeneration,
mut lookup: impl FnMut(LogSource, OpNumber) -> Option<Digest>,
) -> Result<InstallingView, RejectedInstallation> {
let admitted = self.admit_backup(received, generation, &mut lookup);
self.stage(&admitted, Suffix::Streamed)
}
#[expect(
clippy::result_large_err,
reason = "rejection returns protocol ownership without allocation"
)]
pub fn install_primary(
self,
generation: JournalGeneration,
suffix: &[PreparedOperation],
) -> Result<InstallingView, RejectedInstallation> {
let admitted = self
.admit_primary(generation)
.and_then(|admitted| self.validate_suffix(&admitted, suffix));
self.stage(&admitted, Suffix::Supplied(suffix))
}
#[expect(
clippy::result_large_err,
reason = "rejection returns protocol ownership without allocation"
)]
pub fn install_backup(
mut self,
received: ReceivedStart,
generation: JournalGeneration,
suffix: &[PreparedOperation],
mut lookup: impl FnMut(LogSource, OpNumber) -> Option<Digest>,
) -> Result<InstallingView, RejectedInstallation> {
let admitted = self
.admit_backup(received, generation, &mut lookup)
.and_then(|admitted| self.validate_suffix(&admitted, suffix));
self.stage(&admitted, Suffix::Supplied(suffix))
}
fn admit_primary(&self, generation: JournalGeneration) -> Result<Admitted, ViewChangeError> {
let promise = self.require_promised()?;
self.require_primary()?;
let selected = self.selected.ok_or(ViewChangeError::SelectionRequired)?;
Ok(Admitted {
ticket: self.install_ticket(generation, selected.source(), selected.committed())?,
start: StartView {
scope: selected.scope(),
generation,
accepted: selected.source().accepted,
committed: selected.committed(),
},
promise,
body_bytes: 0,
})
}
fn admit_backup(
&mut self,
ReceivedStart { from, start }: ReceivedStart,
generation: JournalGeneration,
lookup: &mut impl FnMut(LogSource, OpNumber) -> Option<Digest>,
) -> Result<Admitted, ViewChangeError> {
self.require_scope(start.scope)?;
let promise = self.require_promised()?;
let voter = self.configuration.voter_index(from)?;
if voter != VoterIndex::primary(start.scope.view) || voter == self.local {
return Err(ReplicationError::WrongRole.into());
}
if !crate::view_change::valid_prefix(start.accepted)
|| !crate::view_change::valid_prefix(start.committed)
|| start.committed.op > start.accepted.op
{
return Err(ViewChangeError::InvalidReport);
}
let source = LogSource {
voter: from,
generation: start.generation,
accepted: start.accepted,
};
let local_commit = self.normal.snapshot().committed;
self.verify_prefix(source, local_commit, lookup)?;
self.verify_prefix(source, start.committed, lookup)?;
let committed = if local_commit.op > start.committed.op {
local_commit
} else {
start.committed
};
Ok(Admitted {
ticket: self.install_ticket(generation, source, committed)?,
start,
promise,
body_bytes: 0,
})
}
fn validate_suffix(
&self,
admitted: &Admitted,
suffix: &[PreparedOperation],
) -> Result<Admitted, ViewChangeError> {
let body_bytes = self.normal.validate_install(admitted.ticket, suffix)?;
Ok(Admitted {
body_bytes,
..*admitted
})
}
fn install_ticket(
&self,
generation: JournalGeneration,
source: LogSource,
committed: Prefix,
) -> Result<InstallTicket, ViewChangeError> {
let previous = self.normal.snapshot();
if generation == previous.journal.generation {
return Err(ViewChangeError::ReusedGeneration);
}
Ok(InstallTicket {
scope: self.scope(),
previous_generation: previous.journal.generation,
generation,
source,
protected_committed: previous.committed,
committed,
})
}
#[expect(
clippy::result_large_err,
reason = "rejection returns protocol ownership without allocation"
)]
fn stage(
self,
admitted: &Result<Admitted, ViewChangeError>,
suffix: Suffix<'_>,
) -> Result<InstallingView, RejectedInstallation> {
let admitted = match *admitted {
Ok(admitted) => admitted,
Err(error) => {
return Err(RejectedInstallation {
view_change: self,
error,
});
}
};
let ticket = admitted.ticket;
let (operations, validated_through) = match suffix {
Suffix::Supplied(operations) => (operations, ticket.accepted()),
Suffix::Streamed => (&[][..], ticket.committed()),
};
let mut normal =
self.normal
.stage_install(ticket, operations, admitted.start, admitted.body_bytes);
if let Suffix::Streamed = suffix {
normal.retain_installed_tail_on_disk();
}
Ok(InstallingView {
ticket,
previous_promise: admitted.promise,
state: InstallState::Pending(normal),
configuration: self.configuration,
requested_view: ticket.scope.view,
validated_through,
})
}
}