1use ozzy_proto::NodeId;
4
5use crate::{
6 Configuration, Digest, JournalGeneration, LogSource, NormalReplica, OpNumber, Prefix,
7 PreparedOperation, ReplicationError, Scope, ViewChange, ViewChangeError,
8};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct StartView {
14 pub scope: Scope,
16 pub generation: JournalGeneration,
18 pub accepted: Prefix,
20 pub committed: Prefix,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct InstallTicket {
31 scope: Scope,
32 previous_generation: JournalGeneration,
33 generation: JournalGeneration,
34 source: LogSource,
35 protected_committed: Prefix,
36 committed: Prefix,
37}
38
39impl InstallTicket {
40 pub const fn scope(self) -> Scope {
42 self.scope
43 }
44 pub const fn previous_generation(self) -> JournalGeneration {
46 self.previous_generation
47 }
48 pub const fn generation(self) -> JournalGeneration {
50 self.generation
51 }
52 pub const fn source(self) -> LogSource {
54 self.source
55 }
56 pub const fn protected_committed(self) -> Prefix {
58 self.protected_committed
59 }
60 pub const fn accepted(self) -> Prefix {
62 self.source.accepted
63 }
64 pub const fn committed(self) -> Prefix {
66 self.committed
67 }
68}
69
70#[derive(Debug)]
72#[expect(
73 clippy::large_enum_variant,
74 reason = "rare ownership transition moves preallocated state without boxing"
75)]
76pub enum InstallOutcome {
77 Normal(NormalReplica),
80 ViewChanging(ViewChange),
82}
83
84#[derive(Debug)]
89pub struct RejectedInstallation {
90 view_change: ViewChange,
91 error: ViewChangeError,
92}
93
94impl RejectedInstallation {
95 pub const fn error(&self) -> ViewChangeError {
97 self.error
98 }
99 pub fn into_view_change(self) -> ViewChange {
101 self.view_change
102 }
103}
104
105impl std::fmt::Display for RejectedInstallation {
106 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 std::fmt::Display::fmt(&self.error, formatter)
108 }
109}
110
111impl std::error::Error for RejectedInstallation {
112 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
113 Some(&self.error)
114 }
115}
116
117#[derive(Debug)]
119pub struct InstallingView {
120 ticket: InstallTicket,
121 previous_promise: crate::PromiseTicket,
122 normal: Option<NormalReplica>,
123 configuration: Configuration,
124 requested_view: u64,
125 failed: bool,
126 validated_through: Prefix,
127}
128
129impl InstallingView {
130 pub(crate) fn requested_scope(&self) -> Scope {
131 Scope {
132 view: self.requested_view,
133 ..self.ticket.scope()
134 }
135 }
136
137 pub const fn ticket(&self) -> InstallTicket {
139 self.ticket
140 }
141
142 pub fn validate_suffix(
150 &mut self,
151 operations: &[PreparedOperation],
152 ) -> Result<(), ViewChangeError> {
153 self.require_pending()?;
154 let (through, _) = self
155 .normal
156 .as_ref()
157 .expect("pending checked")
158 .validate_install_chunk(self.ticket, self.validated_through, operations)?;
159 self.validated_through = through;
160 Ok(())
161 }
162
163 pub fn observe_view(&mut self, from: NodeId, scope: Scope) -> Result<(), ViewChangeError> {
166 self.require_pending()?;
167 self.configuration.voter_index(from)?;
168 if (Scope {
169 view: scope.view,
170 ..self.ticket.scope
171 }) != scope
172 {
173 return Err(ReplicationError::ScopeMismatch.into());
174 }
175 self.requested_view = self.requested_view.max(scope.view);
176 Ok(())
177 }
178
179 pub fn request_view(&mut self, view: u64) -> Result<(), ViewChangeError> {
181 self.require_pending()?;
182 if view <= self.requested_view {
183 return Err(ViewChangeError::ViewNotHigher);
184 }
185 self.requested_view = view;
186 Ok(())
187 }
188
189 pub fn complete(
195 &mut self,
196 ticket: InstallTicket,
197 applied: Prefix,
198 ) -> Result<InstallOutcome, ViewChangeError> {
199 self.require_ticket(ticket)?;
200 if self.validated_through != ticket.accepted() {
201 return Err(ViewChangeError::HistoryMissing);
202 }
203 if applied != ticket.committed {
204 return Err(ViewChangeError::ApplicationPending);
205 }
206 let mut normal = self.normal.take().expect("pending checked");
207 normal.activate_installed();
208 if self.requested_view > ticket.scope.view {
209 Ok(InstallOutcome::ViewChanging(
210 normal.into_view_change(self.requested_view)?,
211 ))
212 } else {
213 Ok(InstallOutcome::Normal(normal))
214 }
215 }
216
217 pub fn complete_abandon(
224 &mut self,
225 ticket: InstallTicket,
226 ) -> Result<ViewChange, ViewChangeError> {
227 self.require_ticket(ticket)?;
228 if self.requested_view <= ticket.scope.view {
229 return Err(ViewChangeError::ViewNotHigher);
230 }
231 ViewChange::after_abandon(
232 self.normal.take().expect("pending checked"),
233 self.previous_promise,
234 self.requested_view,
235 )
236 }
237
238 pub fn fail(&mut self, ticket: InstallTicket) -> Result<(), ViewChangeError> {
240 self.require_ticket(ticket)?;
241 self.failed = true;
242 Ok(())
243 }
244
245 pub(crate) fn require_pending(&self) -> Result<(), ViewChangeError> {
246 if self.failed {
247 return Err(ViewChangeError::Faulted);
248 }
249 if self.normal.is_none() {
250 return Err(ViewChangeError::InstallationCompleted);
251 }
252 Ok(())
253 }
254
255 fn require_ticket(&self, ticket: InstallTicket) -> Result<(), ViewChangeError> {
256 self.require_pending()?;
257 if ticket != self.ticket {
258 return Err(ViewChangeError::StaleInstallation);
259 }
260 Ok(())
261 }
262}
263
264impl ViewChange {
265 #[expect(
272 clippy::result_large_err,
273 reason = "rejection returns owned fenced role"
274 )]
275 pub fn begin_primary_install(
276 self,
277 generation: JournalGeneration,
278 ) -> Result<InstallingView, RejectedInstallation> {
279 let validated = self.primary_install_ticket(generation);
280 self.stage_streaming(&validated)
281 }
282
283 #[expect(
289 clippy::result_large_err,
290 reason = "rejection returns owned fenced role"
291 )]
292 pub fn begin_backup_install(
293 mut self,
294 from: NodeId,
295 start: StartView,
296 generation: JournalGeneration,
297 mut lookup: impl FnMut(LogSource, OpNumber) -> Option<Digest>,
298 ) -> Result<InstallingView, RejectedInstallation> {
299 let validated = self.backup_install_ticket(from, start, generation, &mut lookup);
300 self.stage_streaming(&validated)
301 }
302
303 #[expect(
311 clippy::result_large_err,
312 reason = "rejection returns protocol ownership without allocation"
313 )]
314 pub fn install_primary(
315 self,
316 generation: JournalGeneration,
317 suffix: &[PreparedOperation],
318 ) -> Result<InstallingView, RejectedInstallation> {
319 let validated = (|| {
320 let (ticket, start) = self.primary_install_ticket(generation)?;
321 let bytes = self.normal.validate_install(ticket, suffix)?;
322 Ok((ticket, start, bytes))
323 })();
324 self.stage_install(&validated, suffix)
325 }
326
327 #[expect(
336 clippy::result_large_err,
337 reason = "rejection returns protocol ownership without allocation"
338 )]
339 pub fn install_backup(
340 mut self,
341 from: NodeId,
342 start: StartView,
343 generation: JournalGeneration,
344 suffix: &[PreparedOperation],
345 mut lookup: impl FnMut(LogSource, OpNumber) -> Option<Digest>,
346 ) -> Result<InstallingView, RejectedInstallation> {
347 let validated = (|| {
348 let (ticket, start) =
349 self.backup_install_ticket(from, start, generation, &mut lookup)?;
350 let bytes = self.normal.validate_install(ticket, suffix)?;
351 Ok((ticket, start, bytes))
352 })();
353 self.stage_install(&validated, suffix)
354 }
355
356 fn primary_install_ticket(
357 &self,
358 generation: JournalGeneration,
359 ) -> Result<(InstallTicket, StartView), ViewChangeError> {
360 self.require_promised()?;
361 self.require_primary()?;
362 let selected = self.selected.ok_or(ViewChangeError::SelectionRequired)?;
363 let start = StartView {
364 scope: selected.scope(),
365 generation,
366 accepted: selected.source().accepted,
367 committed: selected.committed(),
368 };
369 Ok((
370 self.install_ticket(generation, selected.source(), selected.committed())?,
371 start,
372 ))
373 }
374
375 fn backup_install_ticket(
376 &mut self,
377 from: NodeId,
378 start: StartView,
379 generation: JournalGeneration,
380 lookup: &mut impl FnMut(LogSource, OpNumber) -> Option<Digest>,
381 ) -> Result<(InstallTicket, StartView), ViewChangeError> {
382 self.require_scope(start.scope)?;
383 self.require_promised()?;
384 self.configuration.voter_index(from)?;
385 if from != self.configuration.primary(start.scope.view)
386 || from == self.configuration.voters()[self.local]
387 {
388 return Err(ReplicationError::WrongRole.into());
389 }
390 if !crate::view_change::valid_prefix(start.accepted)
391 || !crate::view_change::valid_prefix(start.committed)
392 || start.committed.op > start.accepted.op
393 {
394 return Err(ViewChangeError::InvalidReport);
395 }
396 let source = LogSource {
397 voter: from,
398 generation: start.generation,
399 accepted: start.accepted,
400 };
401 let local_commit = self.normal.snapshot().committed;
402 self.verify_prefix(source, local_commit, lookup)?;
403 self.verify_prefix(source, start.committed, lookup)?;
404 let committed = if local_commit.op > start.committed.op {
405 local_commit
406 } else {
407 start.committed
408 };
409 Ok((self.install_ticket(generation, source, committed)?, start))
410 }
411
412 #[expect(
413 clippy::result_large_err,
414 reason = "rejection returns owned fenced role"
415 )]
416 fn stage_streaming(
417 self,
418 validated: &Result<(InstallTicket, StartView), ViewChangeError>,
419 ) -> Result<InstallingView, RejectedInstallation> {
420 let mut installing =
421 self.stage_install(&validated.map(|(ticket, start)| (ticket, start, 0)), &[])?;
422 installing.validated_through = installing.ticket.committed();
423 installing
424 .normal
425 .as_mut()
426 .expect("staged role")
427 .retain_installed_tail_on_disk();
428 Ok(installing)
429 }
430
431 fn install_ticket(
432 &self,
433 generation: JournalGeneration,
434 source: LogSource,
435 committed: Prefix,
436 ) -> Result<InstallTicket, ViewChangeError> {
437 let previous = self.normal.snapshot();
438 if generation == previous.journal.generation {
439 return Err(ViewChangeError::ReusedGeneration);
440 }
441 Ok(InstallTicket {
442 scope: self.scope(),
443 previous_generation: previous.journal.generation,
444 generation,
445 source,
446 protected_committed: previous.committed,
447 committed,
448 })
449 }
450
451 #[expect(
452 clippy::result_large_err,
453 reason = "rejection returns protocol ownership without allocation"
454 )]
455 fn stage_install(
456 self,
457 validated: &Result<(InstallTicket, StartView, usize), ViewChangeError>,
458 suffix: &[PreparedOperation],
459 ) -> Result<InstallingView, RejectedInstallation> {
460 let (ticket, start, bytes) = match *validated {
461 Ok(values) => values,
462 Err(error) => {
463 return Err(RejectedInstallation {
464 view_change: self,
465 error,
466 });
467 }
468 };
469 let previous_promise = self
470 .completed
471 .expect("installation requires durable promise");
472 let normal = self.normal.stage_install(ticket, suffix, start, bytes);
473 Ok(InstallingView {
474 normal: Some(normal),
475 ticket,
476 previous_promise,
477 configuration: self.configuration,
478 requested_view: ticket.scope.view,
479 failed: false,
480 validated_through: ticket.accepted(),
481 })
482 }
483}