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    Prefix, PreparedOperation, ReceivedStart, ReplicationError, Scope, StartView, ViewChangeError,
12    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, Prefix) -> 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, Prefix) -> 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    /// Note progress of this view's election or installation: a looked-up
169    /// batch, a staged chunk, or a local promise or installation I/O still
170    /// running. A lagging copy moves and replays its whole missed suffix in
171    /// the view change, and a slow disk's promise can take as long, which can
172    /// outlast any election timeout, so each advance pushes the local
173    /// election deadline out by one election delay. Peer
174    /// suspicion still counts; two other voters can leave the view without
175    /// this one.
176    pub fn history_progress(&mut self, now: Duration) -> Result<(), DriverError> {
177        self.observe_time(now)?;
178        if matches!(self.role, Role::ViewChanging(_) | Role::Installing(_)) {
179            self.election_deadline = self
180                .election_deadline
181                .max(now.saturating_add(self.election_delay));
182        }
183        Ok(())
184    }
185
186    /// Finish actual selected-log publication and committed application recovery.
187    /// A newer observed/requested view suppresses intermediate normal activation.
188    /// A current installed primary still needs whole-tail quorum and application
189    /// before fresh appends. Failed or stale completions retain phase ownership.
190    pub fn complete_installation(
191        &mut self,
192        ticket: InstallTicket,
193        applied: Prefix,
194        now: Duration,
195    ) -> Result<(), DriverError> {
196        self.observe_time(now)?;
197        let outcome = self.installing_mut()?.complete(ticket, applied)?;
198        let changing = matches!(outcome, InstallOutcome::ViewChanging(_));
199        self.role = match outcome {
200            InstallOutcome::Normal(normal) => Role::Normal(normal),
201            InstallOutcome::ViewChanging(changing) => Role::ViewChanging(changing),
202        };
203        self.contact_deadline = now.saturating_add(self.timing.primary_timeout);
204        self.persistence_deadline = now.saturating_add(self.timing.primary_timeout);
205        self.progress_deadline = now.saturating_add(self.progress_timeout());
206        self.election_deadline = now.saturating_add(self.election_delay);
207        self.retry_at = now;
208        self.start_pending = changing;
209        self.report_pending = changing;
210        self.commit_pending = false;
211        self.activation_pending = VoterSet::EMPTY;
212        Ok(())
213    }
214
215    /// Resume election after the worker successfully aborted unpublished staging.
216    /// All admitted staging I/O must have settled; publication must not have begun.
217    /// A later requested view is required. The old promise/history remain durable,
218    /// but no normal authority or staged bytes survive this transition.
219    pub fn complete_abandon(
220        &mut self,
221        ticket: InstallTicket,
222        now: Duration,
223    ) -> Result<(), DriverError> {
224        self.observe_time(now)?;
225        let changing = self.installing_mut()?.complete_abandon(ticket)?;
226        self.role = Role::ViewChanging(changing);
227        self.election_deadline = now.saturating_add(self.election_delay);
228        self.retry_at = now;
229        self.start_pending = true;
230        self.report_pending = true;
231        self.commit_pending = false;
232        self.activation_pending = VoterSet::EMPTY;
233        Ok(())
234    }
235
236    fn installing_mut(&mut self) -> Result<&mut InstallingView, DriverError> {
237        match &mut self.role {
238            Role::Installing(installing) => Ok(installing),
239            Role::Normal(_) | Role::ViewChanging(_) => Err(ReplicationError::WrongRole.into()),
240            Role::Fenced(_) => Err(ViewChangeError::Faulted.into()),
241        }
242    }
243}