Skip to main content

ozzy_replication/driver/
exit_view.rs

1//! Quorum-supported timeout suspicion, independent of durable election evidence.
2
3use super::{Action, DriverError, ReplicaDriver, Role};
4use crate::wire::Control;
5use crate::{Scope, StartViewChange, VoterIndex, VoterSet};
6use ozzy_proto::NodeId;
7use std::time::Duration;
8
9impl ReplicaDriver {
10    pub(super) fn receive_exit(
11        &mut self,
12        voter: VoterIndex,
13        scope: Scope,
14    ) -> Result<(), DriverError> {
15        self.require_healthy()?;
16        // Neither a distant wish nor a duplicate from another view is evidence
17        // that anyone has entered that view. START_VIEW_CHANGE supplies that fence.
18        if scope.view < self.scope().view {
19            // This voter left that view, possibly on the sender's own request
20            // plus a local timeout, before its suspicion went out. Without
21            // support the sender cannot leave until this voter's durable
22            // promise lets START_VIEW_CHANGE reach it. After that promise,
23            // election traffic carries the newer view while it lasts.
24            //
25            // A peer that also left the view cannot tell this answer from a
26            // request. Answer only the sender, and each sender at most once
27            // per retransmission interval, so two such voters fall silent.
28            if self.now < self.exit_echo_at[voter] {
29                return Ok(());
30            }
31            if self.promise_pending() {
32                self.exit_echo_at[voter] = self.now.saturating_add(self.timing.retransmit);
33                let mut asked = match self.exit_echo {
34                    Some((old, asked)) if old == scope => asked,
35                    _ => VoterSet::EMPTY,
36                };
37                asked.insert(voter);
38                self.exit_echo = Some((scope, asked));
39            } else if matches!(self.role, Role::Normal(_) | Role::Installing(_)) {
40                // Election traffic stops once a view is normal, and normal
41                // traffic carries the view only from its primary. A sender
42                // that cannot hear that primary would never learn of the
43                // view, and with the primary gone the two survivors could
44                // never form another. Answer with this view's durable fence
45                // instead, which brings the sender into it.
46                self.exit_echo_at[voter] = self.now.saturating_add(self.timing.retransmit);
47                self.fence_answers.insert(voter);
48            }
49            return Ok(());
50        }
51        if scope.view > self.scope().view {
52            // A request to leave a newer view says nothing about this one,
53            // but it shows this view is stale. An idle primary has no timeout
54            // of its own, and only the newer view's primary would tell it of
55            // that view. With that primary gone, the sender could never form
56            // a view with it. Ask the sender to leave this view instead: its
57            // answer is the newer view's fence, which brings this voter in.
58            if matches!(self.role, Role::Normal(_)) && self.now >= self.exit_echo_at[voter] {
59                self.exit_echo_at[voter] = self.now.saturating_add(self.timing.retransmit);
60                self.stale_answers.insert(voter);
61            }
62            return Ok(());
63        }
64        if scope != self.scope() {
65            return Ok(());
66        }
67        if let Some(normal) = self.normal() {
68            let snapshot = normal.snapshot();
69            if !self.normal_timed_out(&snapshot) {
70                self.withdraw_exit();
71            }
72        }
73        self.exit_heard_at[voter] = self.now;
74        self.exit_votes.insert(voter);
75        self.advance_if_exit_quorum()
76    }
77
78    /// `from` asked for recovery as a nonvoting copy. When it is the primary of the
79    /// view this replica elects, it cannot install that view, so suspect the
80    /// view now instead of at the election deadline. Leaving the view still
81    /// needs a second voter's suspicion.
82    pub fn observe_recovering(&mut self, from: NodeId, now: Duration) -> Result<(), DriverError> {
83        self.observe_time(now)?;
84        self.require_healthy()?;
85        let voter = self.configuration.voter_index(from)?;
86        if voter != self.local
87            && voter == VoterIndex::primary(self.scope().view)
88            && matches!(self.role, Role::ViewChanging(_))
89        {
90            self.request_exit()?;
91        }
92        Ok(())
93    }
94
95    pub(super) fn request_exit(&mut self) -> Result<(), DriverError> {
96        self.exit_retry_at.get_or_insert(self.now);
97        self.exit_votes.insert(self.local);
98        self.advance_if_exit_quorum()
99    }
100
101    fn advance_if_exit_quorum(&mut self) -> Result<(), DriverError> {
102        // A suspicious peer repeats its request every retransmission
103        // interval. One that recovered goes quiet, so its request lapses
104        // instead of helping a later timeout end a healthy view.
105        for voter in self.local.others() {
106            let heard = self.exit_heard_at[voter];
107            if self.now >= heard.saturating_add(self.timing.primary_timeout) {
108                self.exit_votes.remove(voter);
109            }
110        }
111        if self.exit_votes.len() >= 2 {
112            if self
113                .normal()
114                .is_none_or(|normal| !normal.snapshot().ready_for_appends)
115            {
116                self.election_delay = self
117                    .election_delay
118                    .saturating_mul(2)
119                    .min(self.timing.max_election_timeout);
120            }
121            self.next_view()?;
122        }
123        Ok(())
124    }
125
126    pub(super) fn withdraw_exit(&mut self) {
127        self.exit_votes.remove(self.local);
128        self.exit_retry_at = None;
129    }
130
131    pub(super) fn promise_pending(&self) -> bool {
132        matches!(
133            &self.role,
134            Role::ViewChanging(changing) if changing.promised_view() != changing.scope().view
135        )
136    }
137
138    /// Answer one sender's request to leave another view per call: support
139    /// for an older view's exit, that view's fence, or this stale view's exit.
140    /// The sender retransmits, so a lost answer is repeated after that
141    /// interval.
142    pub(super) fn exit_echo_action(&mut self) -> Option<Action> {
143        if let Some((scope, mut asked)) = self.exit_echo.take()
144            && let Some(voter) = asked.pop_first()
145        {
146            if !asked.is_empty() {
147                self.exit_echo = Some((scope, asked));
148            }
149            return Some(Action::Send {
150                to: self.configuration.voter(voter),
151                message: Control::ExitView(scope),
152            });
153        }
154        if let Some(voter) = self.fence_answers.pop_first() {
155            return Some(Action::Send {
156                to: self.configuration.voter(voter),
157                message: Control::StartViewChange(StartViewChange {
158                    scope: self.scope(),
159                }),
160            });
161        }
162        let voter = self.stale_answers.pop_first()?;
163        Some(Action::Send {
164            to: self.configuration.voter(voter),
165            message: Control::ExitView(self.scope()),
166        })
167    }
168
169    pub(super) fn exit_action(&mut self) -> Option<Action> {
170        if self.now < self.exit_retry_at? {
171            return None;
172        }
173        self.exit_retry_at = Some(self.now.saturating_add(self.timing.retransmit));
174        Some(Action::Broadcast(Control::ExitView(self.scope())))
175    }
176}