mod stage;
use ozzy_proto::NodeId;
use crate::{
Configuration, JournalGeneration, LogSource, NormalReplica, Prefix, PreparedOperation,
PromiseTicket, ReplicationError, Scope, ViewChange, ViewChangeError,
};
#[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,
retained: 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 retained(self) -> Prefix {
self.retained
}
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(())
}
}