Skip to main content

ozzy_replication/normal/
commit.rs

1//! Quorum progress: backup acknowledgments, commit announcements and their
2//! receipt, and the installed view the primary repeats.
3
4use super::NormalReplica;
5use crate::{Commit, Prefix, PrepareOk, ReplicationError};
6use ozzy_proto::NodeId;
7
8impl NormalReplica {
9    /// Construct a backup's cumulative durable vote, never a transport receipt.
10    pub fn acknowledgment(&self) -> Result<PrepareOk, ReplicationError> {
11        self.require_normal()?;
12        if self.is_primary() {
13            return Err(ReplicationError::WrongRole);
14        }
15        Ok(PrepareOk {
16            scope: self.configuration.scope(),
17            durable: self.prefix_at(self.journal.snapshot().durable)?,
18        })
19    }
20
21    /// Count one authenticated backup's cumulative matching durable prefix.
22    /// Duplicate deliveries/reconnections do not create extra voters.
23    pub fn receive_ack(&mut self, from: NodeId, ack: PrepareOk) -> Result<(), ReplicationError> {
24        let (scope, prefix) = (ack.scope, ack.durable);
25        self.require_scope(scope)?;
26        let voter = self.configuration.voter_index(from)?;
27        if !self.is_primary() || voter == self.local {
28            return Err(ReplicationError::WrongRole);
29        }
30        if prefix.op < self.applied.op {
31            return Ok(());
32        }
33        if self.prefix_at(prefix.op)? != prefix {
34            return Err(self.conflict());
35        }
36        self.acknowledged[voter] = self.acknowledged[voter].max(prefix.op);
37        self.advance_commit()
38    }
39
40    /// Construct the primary's current commit announcement or idle heartbeat.
41    pub fn announcement(&self) -> Result<Commit, ReplicationError> {
42        self.require_normal()?;
43        if !self.is_primary() {
44            return Err(ReplicationError::WrongRole);
45        }
46        Ok(Commit {
47            scope: self.configuration.scope(),
48            committed: self.committed,
49        })
50    }
51
52    /// Retransmit the immutable installed-view descriptor after publication.
53    /// Bootstrap has no descriptor. Later appends do not change this snapshot;
54    /// lagging backups install it first, then catch up through normal prepares.
55    pub fn start_view(&self) -> Result<Option<crate::StartView>, ReplicationError> {
56        self.require_normal()?;
57        if !self.is_primary() {
58            return Err(ReplicationError::WrongRole);
59        }
60        Ok(self.installed_view)
61    }
62
63    pub(crate) fn activation_prefix(&self) -> Result<Prefix, ReplicationError> {
64        self.require_normal()?;
65        let snapshot = self.snapshot();
66        if self
67            .installed_view
68            .is_none_or(|installed| installed.accepted != snapshot.accepted)
69            || snapshot.committed != snapshot.accepted
70            || snapshot.journal.durable < snapshot.accepted.op
71            || (self.is_primary() && self.backup_acknowledged() < snapshot.accepted.op)
72        {
73            return Err(ReplicationError::ActivationPending);
74        }
75        Ok(snapshot.accepted)
76    }
77
78    pub(crate) fn observe_installed_start(
79        &mut self,
80        from: NodeId,
81        start: crate::StartView,
82    ) -> Result<(), ReplicationError> {
83        self.require_scope(start.scope)?;
84        self.require_primary(from)?;
85        if self.is_primary() {
86            return Err(ReplicationError::WrongRole);
87        }
88        if self.installed_view != Some(start) {
89            return Err(self.conflict());
90        }
91        Ok(())
92    }
93
94    /// Observe an authenticated primary's commit, including piggybacked commit.
95    /// Visibility waits for this backup's configured local evidence. Missing
96    /// payload returns `HistoryGap`; retry the announcement after catch-up.
97    /// An announcement past the accepted prefix still commits everything this
98    /// backup accepted in the view, so that much becomes visible at once.
99    pub fn receive_commit(&mut self, from: NodeId, commit: Commit) -> Result<(), ReplicationError> {
100        self.require_scope(commit.scope)?;
101        self.require_primary(from)?;
102        if self.is_primary() {
103            return Err(ReplicationError::WrongRole);
104        }
105        if commit.committed.op < self.applied.op {
106            return Ok(());
107        }
108        if commit.committed.op > self.accepted.op
109            && self.journal_tail.is_none()
110            && self.accepted.op > self.known_commit
111        {
112            // Otherwise a backup whose window is full behind a primary that
113            // already committed past it never learns a commit it can apply,
114            // and never releases the window the next suffix needs.
115            self.known_commit = self.accepted.op;
116            self.advance_commit()?;
117        }
118        if self.prefix_at(commit.committed.op)? != commit.committed {
119            return Err(self.conflict());
120        }
121        self.known_commit = self.known_commit.max(commit.committed.op);
122        self.advance_commit()
123    }
124}