Skip to main content

ozzy_replication/driver/
installation.rs

1//! Keep protocol ownership and newer-view fences alive across asynchronous installation.
2
3use std::time::Duration;
4
5use ozzy_proto::NodeId;
6
7use super::{Action, DriverError, ReplicaDriver, Role};
8use crate::wire::Control;
9use crate::{
10    Commit, Digest, InstallOutcome, InstallTicket, InstallingView, JournalGeneration, LogSource,
11    OpNumber, Prefix, PreparedOperation, ReceivedStart, ReplicationError, Scope, StartView,
12    ViewChangeError, VoterSet,
13};
14
15/// Whole-tail commit authority captured before worker-side image activation.
16/// This is not application completion. Recheck it after I/O before releasing admission.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct ActivationTicket {
19    local: NodeId,
20    scope: Scope,
21    generation: JournalGeneration,
22    through: Prefix,
23    applied: Prefix,
24}
25
26impl ActivationTicket {
27    /// Local configured voter whose application image may be activated.
28    pub const fn local(self) -> NodeId {
29        self.local
30    }
31    /// Exact installed configuration and view.
32    pub const fn scope(self) -> Scope {
33        self.scope
34    }
35    /// Installed writer incarnation; old generations cannot activate it.
36    pub const fn generation(self) -> JournalGeneration {
37        self.generation
38    }
39    /// Entire selected tail, committed and locally durable.
40    pub const fn through(self) -> Prefix {
41        self.through
42    }
43    /// Previously completed application boundary.
44    pub const fn applied(self) -> Prefix {
45        self.applied
46    }
47}
48
49impl ReplicaDriver {
50    /// Send one pending new-view activation commit. A voter set only while an
51    /// installed view exists, so a missing view drops the pending set.
52    pub(super) fn activation_action(&mut self) -> Result<Option<Action>, DriverError> {
53        if self.activation_pending.is_empty() {
54            return Ok(None);
55        }
56        let Some(start) = self.require_normal()?.start_view()? else {
57            self.activation_pending = VoterSet::EMPTY;
58            return Ok(None);
59        };
60        let Some(voter) = self.activation_pending.pop_first() else {
61            return Ok(None);
62        };
63        Ok(Some(Action::Send {
64            to: self.configuration.voter(voter),
65            message: Control::Commit(Commit {
66                scope: start.scope,
67                committed: start.accepted,
68            }),
69        }))
70    }
71
72    /// Authorize worker-side activation of an installed, whole-tail committed image.
73    /// The primary additionally requires a backup's new-view durable ACK, even
74    /// when the selected tail was already known committed before installation.
75    pub fn begin_activation(&self) -> Result<ActivationTicket, DriverError> {
76        let normal = self.normal().ok_or(ReplicationError::NotNormal)?;
77        let through = normal.activation_prefix()?;
78        let snapshot = normal.snapshot();
79        Ok(ActivationTicket {
80            local: self.configuration.voter(self.local),
81            scope: snapshot.scope,
82            generation: snapshot.journal.generation,
83            through,
84            applied: snapshot.applied,
85        })
86    }
87
88    /// Report completed image activation only while its captured authority is current.
89    /// A newer view or changed image rejects a late completion without restoring voting.
90    pub fn complete_activation(&mut self, ticket: ActivationTicket) -> Result<(), DriverError> {
91        if self.begin_activation().ok() != Some(ticket) {
92            return Err(DriverError::StaleValidation);
93        }
94        self.apply_through(ticket.through)
95    }
96
97    /// Inspect the exact pending selected-log action without granting normal authority.
98    pub fn installation_ticket(&self) -> Option<InstallTicket> {
99        match &self.role {
100            Role::Installing(installing) => Some(installing.ticket()),
101            Role::Normal(_) | Role::ViewChanging(_) | Role::Fenced(_) => None,
102        }
103    }
104
105    /// Begin primary installation after verified quorum selection. The adapter
106    /// supplies a fresh writer incarnation and performs bounded staging, sync,
107    /// publication, and application recovery on the storage worker.
108    /// Rejection retains the original fenced role and any missing-history state.
109    pub fn begin_primary_install(
110        &mut self,
111        generation: JournalGeneration,
112    ) -> Result<InstallTicket, DriverError> {
113        self.begin_install(None, generation, |_, _| None)
114    }
115
116    /// Begin backup publication after an authenticated current-primary descriptor
117    /// and this backup's completed durable promise. History lookup must be pinned
118    /// and nonblocking; missing evidence retains the fenced role for later retry.
119    pub fn begin_backup_install(
120        &mut self,
121        from: NodeId,
122        start: StartView,
123        generation: JournalGeneration,
124        lookup: impl FnMut(LogSource, OpNumber) -> Option<Digest>,
125    ) -> Result<InstallTicket, DriverError> {
126        self.begin_install(Some(ReceivedStart { from, start }), generation, lookup)
127    }
128
129    fn begin_install(
130        &mut self,
131        backup: Option<ReceivedStart>,
132        generation: JournalGeneration,
133        lookup: impl FnMut(LogSource, OpNumber) -> Option<Digest>,
134    ) -> Result<InstallTicket, DriverError> {
135        self.changing()?;
136        let Role::ViewChanging(changing) = self.take_role() else {
137            return Err(ViewChangeError::Faulted.into());
138        };
139        let admitted = match backup {
140            Some(received) => changing.begin_backup_install(received, generation, lookup),
141            None => changing.begin_primary_install(generation),
142        };
143        match admitted {
144            Ok(installing) => {
145                let ticket = installing.ticket();
146                self.role = Role::Installing(installing);
147                self.election_deadline = self.now.saturating_add(self.election_delay);
148                Ok(ticket)
149            }
150            Err(rejected) => {
151                let error = rejected.error();
152                self.role = Role::ViewChanging(rejected.into_view_change());
153                Err(error.into())
154            }
155        }
156    }
157
158    /// Validate the next bounded selected suffix chunk, independently of its disk
159    /// completion. The adapter must validate bodies/application transitions and
160    /// stage those same bytes; this alone is never durability or commit evidence.
161    pub fn validate_install_suffix(
162        &mut self,
163        operations: &[PreparedOperation],
164    ) -> Result<(), DriverError> {
165        Ok(self.installing_mut()?.validate_suffix(operations)?)
166    }
167
168    /// Finish actual selected-log publication and committed application recovery.
169    /// A newer observed/requested view suppresses intermediate normal activation.
170    /// A current installed primary still needs whole-tail quorum and application
171    /// before fresh appends. Failed or stale completions retain phase ownership.
172    pub fn complete_installation(
173        &mut self,
174        ticket: InstallTicket,
175        applied: Prefix,
176        now: Duration,
177    ) -> Result<(), DriverError> {
178        self.observe_time(now)?;
179        let outcome = self.installing_mut()?.complete(ticket, applied)?;
180        let changing = matches!(outcome, InstallOutcome::ViewChanging(_));
181        self.role = match outcome {
182            InstallOutcome::Normal(normal) => Role::Normal(normal),
183            InstallOutcome::ViewChanging(changing) => Role::ViewChanging(changing),
184        };
185        self.contact_deadline = now.saturating_add(self.timing.primary_timeout);
186        self.persistence_deadline = now.saturating_add(self.timing.primary_timeout);
187        self.progress_deadline = now.saturating_add(self.progress_timeout());
188        self.election_deadline = now.saturating_add(self.election_delay);
189        self.retry_at = now;
190        self.start_pending = changing;
191        self.report_pending = changing;
192        self.commit_pending = false;
193        self.activation_pending = VoterSet::EMPTY;
194        Ok(())
195    }
196
197    /// Resume election after the worker successfully aborted unpublished staging.
198    /// All admitted staging I/O must have settled; publication must not have begun.
199    /// A later requested view is required. The old promise/history remain durable,
200    /// but no normal authority or staged bytes survive this transition.
201    pub fn complete_abandon(
202        &mut self,
203        ticket: InstallTicket,
204        now: Duration,
205    ) -> Result<(), DriverError> {
206        self.observe_time(now)?;
207        let changing = self.installing_mut()?.complete_abandon(ticket)?;
208        self.role = Role::ViewChanging(changing);
209        self.election_deadline = now.saturating_add(self.election_delay);
210        self.retry_at = now;
211        self.start_pending = true;
212        self.report_pending = true;
213        self.commit_pending = false;
214        self.activation_pending = VoterSet::EMPTY;
215        Ok(())
216    }
217
218    fn installing_mut(&mut self) -> Result<&mut InstallingView, DriverError> {
219        match &mut self.role {
220            Role::Installing(installing) => Ok(installing),
221            Role::Normal(_) | Role::ViewChanging(_) => Err(ReplicationError::WrongRole.into()),
222            Role::Fenced(_) => Err(ViewChangeError::Faulted.into()),
223        }
224    }
225}