Skip to main content

ozzy_replication/view_change/
recovery.rs

1//! Re-entering an election after restart or an abandoned installation,
2//! without granting any same-view authority.
3
4use super::{
5    DoViewChange, Promise, PromiseTicket, RecoveredState, ViewChange, ViewChangeError,
6    validate_report,
7};
8use crate::{
9    Configuration, JournalGeneration, NormalReplica, PipelineLimits, ReplicationError, Scope,
10};
11use ozzy_proto::NodeId;
12
13impl ViewChange {
14    pub(crate) fn after_abandon(
15        mut normal: NormalReplica,
16        previous: PromiseTicket,
17        requested_view: u64,
18    ) -> Result<Self, ViewChangeError> {
19        normal.restore_fenced(previous.generation(), previous.log());
20        let mut changing = normal.into_view_change(requested_view)?;
21        changing.promise = Promise::Completed(previous);
22        Ok(changing)
23    }
24
25    /// Restart an intact durable voter without granting same-view authority.
26    ///
27    /// Restart from an installed normal view requests its successor, fencing a
28    /// primary that sent prepares before its local write completed. An already
29    /// promised but never installed election resumes that same view: it could
30    /// not have assigned normal operations there. Repeated restarts cannot
31    /// ratchet that unfinished election. Quorum selection and durable selected
32    /// installation/activation remain mandatory before any normal authority.
33    ///
34    /// `generation` must be a fresh writer incarnation. Journal recovery and
35    /// validation run on the storage worker before calling this sans-I/O method.
36    /// Retained operations stay on disk; only the live pipeline is preallocated.
37    /// Canonical application recovery is deferred to selected-view installation.
38    pub fn recover_intact(
39        configuration: Configuration,
40        local: NodeId,
41        generation: JournalGeneration,
42        recovered: RecoveredState,
43        limits: PipelineLimits,
44    ) -> Result<Self, ViewChangeError> {
45        if recovered.scope
46            != (Scope {
47                view: recovered.scope.view,
48                ..configuration.scope()
49            })
50        {
51            return Err(ReplicationError::ScopeMismatch.into());
52        }
53        if recovered.log.last_normal_view > recovered.scope.view {
54            return Err(ViewChangeError::InvalidReport);
55        }
56        let view = if recovered.scope.view > recovered.log.last_normal_view {
57            recovered.scope.view
58        } else {
59            recovered
60                .scope
61                .view
62                .checked_add(1)
63                .ok_or(ViewChangeError::ViewExhausted)?
64        };
65        validate_report(DoViewChange {
66            scope: Scope {
67                view,
68                ..recovered.scope
69            },
70            generation,
71            log: recovered.log,
72        })?;
73        let normal =
74            NormalReplica::recover_fenced(configuration, local, generation, recovered.log, limits)?;
75        let mut changing = normal.into_view_change(view)?;
76        changing.promise = if recovered.scope.view == view {
77            changing.starts.insert(changing.local); // Independently recovered durable promise.
78            Promise::Completed(PromiseTicket {
79                scope: changing.scope(),
80                generation,
81                log: recovered.log,
82            })
83        } else {
84            Promise::Prior(recovered.scope.view)
85        };
86        Ok(changing)
87    }
88}