Skip to main content

ozzy_replication/driver/
receive.rs

1//! Inbound peer traffic: election and commit controls, and prepares from
2//! the current primary.
3
4use super::{DriverError, ReplicaDriver, Role};
5use crate::wire::Control;
6use crate::{
7    Admission, PreparedOperation, ReplicationError, Scope, StartView, ViewChangeError, VoterIndex,
8};
9use ozzy_proto::NodeId;
10use std::time::Duration;
11
12impl ReplicaDriver {
13    /// Receive one authenticated, wire-validated control. Stale views are ignored.
14    /// A timeout only sends volatile `EXIT_VIEW` suspicion until a quorum agrees.
15    /// A durably promised `START_VIEW_CHANGE` instead fences older authority,
16    /// including an intact voter's restart request. These distinct messages must
17    /// not be interchanged. Senders must obey the crash-fault protocol, not merely
18    /// supply a claimed node identity.
19    ///
20    /// A returned `START_VIEW` needs bounded retention by the adapter until its
21    /// local promise completes, then verified selected installation. It never
22    /// activates a normal role here. Exact duplicate installed descriptors return
23    /// `None` without reinstalling and permit replay of the normal durable ACK.
24    /// Normal ACK grants remain adapter-owned.
25    pub fn receive(
26        &mut self,
27        from: NodeId,
28        message: Control,
29        now: Duration,
30    ) -> Result<Option<StartView>, DriverError> {
31        self.observe_time(now)?;
32        let voter = self.configuration.voter_index(from)?;
33        if voter == self.local {
34            return Err(ReplicationError::WrongRole.into());
35        }
36        let scope = message.scope();
37        if scope
38            != (Scope {
39                view: scope.view,
40                ..self.configuration.scope()
41            })
42        {
43            return Err(ReplicationError::ScopeMismatch.into());
44        }
45        if let Control::ExitView(_) = message {
46            self.receive_exit(voter, scope)?;
47            return Ok(None);
48        }
49        if scope.view < self.scope().view {
50            return Ok(None);
51        }
52        self.check_role(voter, message)?;
53        if let Control::StartViewChange(_) = message {
54            self.require_healthy()?;
55            // A first-phase sender has durably fenced its previous authority.
56            // Timeout-only suspicion uses EXIT_VIEW and cannot reach this path.
57            if scope.view > self.scope().view {
58                self.change_view(scope.view)?;
59            }
60            if scope.view != self.scope().view {
61                return Ok(None);
62            }
63        }
64        if scope.view > self.scope().view {
65            self.change_view(scope.view)?;
66        }
67        match &mut self.role {
68            Role::Normal(_) => self.receive_normal(from, voter, message, now)?,
69            Role::ViewChanging(changing) => match message {
70                Control::StartViewChange(start) => changing.receive_start(from, start)?,
71                Control::DoViewChange(report) => changing.receive_report(from, report)?,
72                Control::StartView(start) => return Ok(Some(start)),
73                Control::Commit(_) | Control::PrepareOk { .. } | Control::ExitView(_) => {}
74            },
75            Role::Installing(installing) => installing.observe_view(from, scope)?,
76            Role::Fenced(_) => return Err(ViewChangeError::Faulted.into()),
77        }
78        Ok(None)
79    }
80
81    /// Only the primary announces commits and installed views, and only the
82    /// primary collects acknowledgments and election reports.
83    fn check_role(&self, voter: VoterIndex, message: Control) -> Result<(), DriverError> {
84        let primary = VoterIndex::primary(message.scope().view);
85        match message {
86            Control::Commit(_) | Control::StartView(_) if voter != primary => {
87                Err(ReplicationError::WrongRole.into())
88            }
89            Control::PrepareOk { .. } | Control::DoViewChange(_) if self.local != primary => {
90                Err(ReplicationError::WrongRole.into())
91            }
92            _ => Ok(()),
93        }
94    }
95
96    /// Apply a current-view commit, acknowledgment, or installed view to the
97    /// normal role, and restart the deadlines its progress satisfies.
98    fn receive_normal(
99        &mut self,
100        from: NodeId,
101        voter: VoterIndex,
102        message: Control,
103        now: Duration,
104    ) -> Result<(), DriverError> {
105        let Role::Normal(normal) = &mut self.role else {
106            return Ok(());
107        };
108        let committed = normal.snapshot().committed;
109        match message {
110            Control::Commit(commit) => {
111                normal.receive_commit(from, commit)?;
112                self.contact_deadline = now.saturating_add(self.timing.primary_timeout);
113            }
114            Control::PrepareOk { ack, .. } => normal.receive_ack(from, ack)?,
115            Control::StartView(start) => {
116                normal.observe_installed_start(from, start)?;
117                self.contact_deadline = now.saturating_add(self.timing.primary_timeout);
118            }
119            _ => return Ok(()),
120        }
121        if let Some(prefix) = message.acknowledged()
122            && let Some(start) = normal.start_view()?
123            && prefix == start.accepted
124            && start.accepted.op < normal.snapshot().committed.op
125        {
126            // Selected history plus an established quorum authorizes
127            // replay. An old vote alone cannot create commit authority.
128            self.activation_pending.insert(voter);
129        }
130        if normal.snapshot().committed != committed {
131            self.progress_deadline = now.saturating_add(self.progress_timeout());
132        }
133        Ok(())
134    }
135
136    /// Admit an already body/application-validated canonical group. The returned
137    /// ticket schedules disk work; transmission may overlap that work. Later
138    /// views require selected installation, never implicit normal activation.
139    pub fn prepare(
140        &mut self,
141        from: NodeId,
142        scope: Scope,
143        operations: &[PreparedOperation],
144        now: Duration,
145    ) -> Result<Admission, DriverError> {
146        self.observe_time(now)?;
147        let voter = self.configuration.voter_index(from)?;
148        if scope
149            != (Scope {
150                view: scope.view,
151                ..self.configuration.scope()
152            })
153        {
154            return Err(ReplicationError::ScopeMismatch.into());
155        }
156        if voter != VoterIndex::primary(scope.view) {
157            return Err(ReplicationError::WrongRole.into());
158        }
159        if scope.view > self.scope().view {
160            self.change_view(scope.view)?;
161        }
162        let Role::Normal(normal) = &mut self.role else {
163            return Err(ReplicationError::NotNormal.into());
164        };
165        let previous = normal.snapshot();
166        let admitted = normal.prepare(from, scope, operations)?;
167        if normal.snapshot().accepted != previous.accepted
168            && previous.accepted == previous.committed
169            && previous.ready_for_appends
170        {
171            self.progress_deadline = now.saturating_add(self.timing.primary_timeout);
172        }
173        if normal.snapshot().accepted != previous.accepted
174            && previous.accepted.op == previous.journal.durable
175        {
176            self.persistence_deadline = now.saturating_add(self.timing.primary_timeout);
177        }
178        self.contact_deadline = now.saturating_add(self.timing.primary_timeout);
179        Ok(admitted)
180    }
181}