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