1use ozzy_proto::NodeId;
4
5use crate::{
6 Configuration, Digest, JournalGeneration, LogSource, NormalReplica, OpNumber, Prefix,
7 PreparedOperation, PromiseTicket, ReplicationError, Scope, ViewChange, ViewChangeError,
8 VoterIndex,
9};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct StartView {
15 pub scope: Scope,
17 pub generation: JournalGeneration,
19 pub accepted: Prefix,
21 pub committed: Prefix,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct ReceivedStart {
29 pub from: NodeId,
31 pub start: StartView,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub struct InstallTicket {
42 scope: Scope,
43 previous_generation: JournalGeneration,
44 generation: JournalGeneration,
45 source: LogSource,
46 protected_committed: Prefix,
47 committed: Prefix,
48}
49
50impl InstallTicket {
51 pub const fn scope(self) -> Scope {
53 self.scope
54 }
55 pub const fn previous_generation(self) -> JournalGeneration {
57 self.previous_generation
58 }
59 pub const fn generation(self) -> JournalGeneration {
61 self.generation
62 }
63 pub const fn source(self) -> LogSource {
65 self.source
66 }
67 pub const fn protected_committed(self) -> Prefix {
69 self.protected_committed
70 }
71 pub const fn accepted(self) -> Prefix {
73 self.source.accepted
74 }
75 pub const fn committed(self) -> Prefix {
77 self.committed
78 }
79}
80
81#[derive(Debug)]
83#[expect(
84 clippy::large_enum_variant,
85 reason = "rare ownership transition moves preallocated state without boxing"
86)]
87pub enum InstallOutcome {
88 Normal(NormalReplica),
91 ViewChanging(ViewChange),
93}
94
95#[derive(Debug)]
100pub struct RejectedInstallation {
101 view_change: ViewChange,
102 error: ViewChangeError,
103}
104
105impl RejectedInstallation {
106 pub const fn error(&self) -> ViewChangeError {
108 self.error
109 }
110 pub fn into_view_change(self) -> ViewChange {
112 self.view_change
113 }
114}
115
116impl std::fmt::Display for RejectedInstallation {
117 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 std::fmt::Display::fmt(&self.error, formatter)
119 }
120}
121
122impl std::error::Error for RejectedInstallation {
123 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
124 Some(&self.error)
125 }
126}
127
128#[derive(Debug)]
130pub struct InstallingView {
131 ticket: InstallTicket,
132 previous_promise: PromiseTicket,
133 state: InstallState,
134 configuration: Configuration,
135 requested_view: u64,
136 validated_through: Prefix,
137}
138
139#[derive(Debug)]
141#[expect(
142 clippy::large_enum_variant,
143 reason = "the staged role moves without boxing"
144)]
145enum InstallState {
146 Pending(NormalReplica),
147 Failed,
149 Completed,
151}
152
153impl InstallState {
154 fn pending(&self) -> Result<&NormalReplica, ViewChangeError> {
155 match self {
156 Self::Pending(normal) => Ok(normal),
157 Self::Failed => Err(ViewChangeError::Faulted),
158 Self::Completed => Err(ViewChangeError::InstallationCompleted),
159 }
160 }
161
162 fn take(&mut self) -> Result<NormalReplica, ViewChangeError> {
164 match std::mem::replace(self, Self::Completed) {
165 Self::Pending(normal) => Ok(normal),
166 Self::Failed => {
167 *self = Self::Failed;
168 Err(ViewChangeError::Faulted)
169 }
170 Self::Completed => Err(ViewChangeError::InstallationCompleted),
171 }
172 }
173}
174
175impl InstallingView {
176 pub(crate) fn requested_scope(&self) -> Scope {
177 Scope {
178 view: self.requested_view,
179 ..self.ticket.scope()
180 }
181 }
182
183 pub const fn ticket(&self) -> InstallTicket {
185 self.ticket
186 }
187
188 pub fn validate_suffix(
196 &mut self,
197 operations: &[PreparedOperation],
198 ) -> Result<(), ViewChangeError> {
199 let (through, _) = self.state.pending()?.validate_install_chunk(
200 self.ticket,
201 self.validated_through,
202 operations,
203 )?;
204 self.validated_through = through;
205 Ok(())
206 }
207
208 pub fn observe_view(&mut self, from: NodeId, scope: Scope) -> Result<(), ViewChangeError> {
211 self.require_pending()?;
212 self.configuration.voter_index(from)?;
213 if (Scope {
214 view: scope.view,
215 ..self.ticket.scope
216 }) != scope
217 {
218 return Err(ReplicationError::ScopeMismatch.into());
219 }
220 self.requested_view = self.requested_view.max(scope.view);
221 Ok(())
222 }
223
224 pub fn request_view(&mut self, view: u64) -> Result<(), ViewChangeError> {
226 self.require_pending()?;
227 if view <= self.requested_view {
228 return Err(ViewChangeError::ViewNotHigher);
229 }
230 self.requested_view = view;
231 Ok(())
232 }
233
234 pub fn complete(
240 &mut self,
241 ticket: InstallTicket,
242 applied: Prefix,
243 ) -> Result<InstallOutcome, ViewChangeError> {
244 self.require_ticket(ticket)?;
245 if self.validated_through != ticket.accepted() {
246 return Err(ViewChangeError::HistoryMissing);
247 }
248 if applied != ticket.committed {
249 return Err(ViewChangeError::ApplicationPending);
250 }
251 let mut normal = self.state.take()?;
252 normal.activate_installed();
253 if self.requested_view > ticket.scope.view {
254 Ok(InstallOutcome::ViewChanging(
255 normal.into_view_change(self.requested_view)?,
256 ))
257 } else {
258 Ok(InstallOutcome::Normal(normal))
259 }
260 }
261
262 pub fn complete_abandon(
269 &mut self,
270 ticket: InstallTicket,
271 ) -> Result<ViewChange, ViewChangeError> {
272 self.require_ticket(ticket)?;
273 if self.requested_view <= ticket.scope.view {
274 return Err(ViewChangeError::ViewNotHigher);
275 }
276 ViewChange::after_abandon(
277 self.state.take()?,
278 self.previous_promise,
279 self.requested_view,
280 )
281 }
282
283 pub fn fail(&mut self, ticket: InstallTicket) -> Result<(), ViewChangeError> {
285 self.require_ticket(ticket)?;
286 self.state = InstallState::Failed;
287 Ok(())
288 }
289
290 pub(crate) fn require_pending(&self) -> Result<(), ViewChangeError> {
291 self.state.pending().map(|_| ())
292 }
293
294 fn require_ticket(&self, ticket: InstallTicket) -> Result<(), ViewChangeError> {
295 self.require_pending()?;
296 if ticket != self.ticket {
297 return Err(ViewChangeError::StaleInstallation);
298 }
299 Ok(())
300 }
301}
302
303#[derive(Debug, Clone, Copy)]
305struct Admitted {
306 ticket: InstallTicket,
307 start: StartView,
308 promise: PromiseTicket,
309 body_bytes: usize,
310}
311
312#[derive(Debug, Clone, Copy)]
314enum Suffix<'a> {
315 Supplied(&'a [PreparedOperation]),
317 Streamed,
319}
320
321impl ViewChange {
322 #[expect(
329 clippy::result_large_err,
330 reason = "rejection returns owned fenced role"
331 )]
332 pub fn begin_primary_install(
333 self,
334 generation: JournalGeneration,
335 ) -> Result<InstallingView, RejectedInstallation> {
336 let admitted = self.admit_primary(generation);
337 self.stage(&admitted, Suffix::Streamed)
338 }
339
340 #[expect(
346 clippy::result_large_err,
347 reason = "rejection returns owned fenced role"
348 )]
349 pub fn begin_backup_install(
350 mut self,
351 received: ReceivedStart,
352 generation: JournalGeneration,
353 mut lookup: impl FnMut(LogSource, OpNumber) -> Option<Digest>,
354 ) -> Result<InstallingView, RejectedInstallation> {
355 let admitted = self.admit_backup(received, generation, &mut lookup);
356 self.stage(&admitted, Suffix::Streamed)
357 }
358
359 #[expect(
367 clippy::result_large_err,
368 reason = "rejection returns protocol ownership without allocation"
369 )]
370 pub fn install_primary(
371 self,
372 generation: JournalGeneration,
373 suffix: &[PreparedOperation],
374 ) -> Result<InstallingView, RejectedInstallation> {
375 let admitted = self
376 .admit_primary(generation)
377 .and_then(|admitted| self.validate_suffix(&admitted, suffix));
378 self.stage(&admitted, Suffix::Supplied(suffix))
379 }
380
381 #[expect(
390 clippy::result_large_err,
391 reason = "rejection returns protocol ownership without allocation"
392 )]
393 pub fn install_backup(
394 mut self,
395 received: ReceivedStart,
396 generation: JournalGeneration,
397 suffix: &[PreparedOperation],
398 mut lookup: impl FnMut(LogSource, OpNumber) -> Option<Digest>,
399 ) -> Result<InstallingView, RejectedInstallation> {
400 let admitted = self
401 .admit_backup(received, generation, &mut lookup)
402 .and_then(|admitted| self.validate_suffix(&admitted, suffix));
403 self.stage(&admitted, Suffix::Supplied(suffix))
404 }
405
406 fn admit_primary(&self, generation: JournalGeneration) -> Result<Admitted, ViewChangeError> {
407 let promise = self.require_promised()?;
408 self.require_primary()?;
409 let selected = self.selected.ok_or(ViewChangeError::SelectionRequired)?;
410 Ok(Admitted {
411 ticket: self.install_ticket(generation, selected.source(), selected.committed())?,
412 start: StartView {
413 scope: selected.scope(),
414 generation,
415 accepted: selected.source().accepted,
416 committed: selected.committed(),
417 },
418 promise,
419 body_bytes: 0,
420 })
421 }
422
423 fn admit_backup(
424 &mut self,
425 ReceivedStart { from, start }: ReceivedStart,
426 generation: JournalGeneration,
427 lookup: &mut impl FnMut(LogSource, OpNumber) -> Option<Digest>,
428 ) -> Result<Admitted, ViewChangeError> {
429 self.require_scope(start.scope)?;
430 let promise = self.require_promised()?;
431 let voter = self.configuration.voter_index(from)?;
432 if voter != VoterIndex::primary(start.scope.view) || voter == self.local {
433 return Err(ReplicationError::WrongRole.into());
434 }
435 if !crate::view_change::valid_prefix(start.accepted)
436 || !crate::view_change::valid_prefix(start.committed)
437 || start.committed.op > start.accepted.op
438 {
439 return Err(ViewChangeError::InvalidReport);
440 }
441 let source = LogSource {
442 voter: from,
443 generation: start.generation,
444 accepted: start.accepted,
445 };
446 let local_commit = self.normal.snapshot().committed;
447 self.verify_prefix(source, local_commit, lookup)?;
448 self.verify_prefix(source, start.committed, lookup)?;
449 let committed = if local_commit.op > start.committed.op {
450 local_commit
451 } else {
452 start.committed
453 };
454 Ok(Admitted {
455 ticket: self.install_ticket(generation, source, committed)?,
456 start,
457 promise,
458 body_bytes: 0,
459 })
460 }
461
462 fn validate_suffix(
463 &self,
464 admitted: &Admitted,
465 suffix: &[PreparedOperation],
466 ) -> Result<Admitted, ViewChangeError> {
467 let body_bytes = self.normal.validate_install(admitted.ticket, suffix)?;
468 Ok(Admitted {
469 body_bytes,
470 ..*admitted
471 })
472 }
473
474 fn install_ticket(
475 &self,
476 generation: JournalGeneration,
477 source: LogSource,
478 committed: Prefix,
479 ) -> Result<InstallTicket, ViewChangeError> {
480 let previous = self.normal.snapshot();
481 if generation == previous.journal.generation {
482 return Err(ViewChangeError::ReusedGeneration);
483 }
484 Ok(InstallTicket {
485 scope: self.scope(),
486 previous_generation: previous.journal.generation,
487 generation,
488 source,
489 protected_committed: previous.committed,
490 committed,
491 })
492 }
493
494 #[expect(
495 clippy::result_large_err,
496 reason = "rejection returns protocol ownership without allocation"
497 )]
498 fn stage(
499 self,
500 admitted: &Result<Admitted, ViewChangeError>,
501 suffix: Suffix<'_>,
502 ) -> Result<InstallingView, RejectedInstallation> {
503 let admitted = match *admitted {
504 Ok(admitted) => admitted,
505 Err(error) => {
506 return Err(RejectedInstallation {
507 view_change: self,
508 error,
509 });
510 }
511 };
512 let ticket = admitted.ticket;
513 let (operations, validated_through) = match suffix {
514 Suffix::Supplied(operations) => (operations, ticket.accepted()),
515 Suffix::Streamed => (&[][..], ticket.committed()),
516 };
517 let mut normal =
518 self.normal
519 .stage_install(ticket, operations, admitted.start, admitted.body_bytes);
520 if let Suffix::Streamed = suffix {
521 normal.retain_installed_tail_on_disk();
522 }
523 Ok(InstallingView {
524 ticket,
525 previous_promise: admitted.promise,
526 state: InstallState::Pending(normal),
527 configuration: self.configuration,
528 requested_view: ticket.scope.view,
529 validated_through,
530 })
531 }
532}