1use ozzy_proto::NodeId;
4
5use crate::{
6 Configuration, Digest, JournalGeneration, NormalReplica, OpNumber, PipelineLimits, Prefix,
7 ReplicaSnapshot, ReplicationError, Scope, Status, SyncTicket, WriteTicket,
8};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct FrozenLog {
17 pub last_normal_view: u64,
19 pub accepted: Prefix,
21 pub committed: Prefix,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct RecoveredState {
33 pub scope: Scope,
35 pub log: FrozenLog,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub struct PromiseTicket {
47 scope: Scope,
48 generation: JournalGeneration,
49 log: FrozenLog,
50}
51
52impl PromiseTicket {
53 pub const fn scope(self) -> Scope {
55 self.scope
56 }
57 pub const fn generation(self) -> JournalGeneration {
59 self.generation
60 }
61 pub const fn log(self) -> FrozenLog {
63 self.log
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub struct StartViewChange {
70 pub scope: Scope,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub struct DoViewChange {
77 pub scope: Scope,
79 pub generation: JournalGeneration,
81 pub log: FrozenLog,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub struct LogSource {
88 pub voter: NodeId,
90 pub generation: JournalGeneration,
92 pub accepted: Prefix,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub struct SelectedView {
102 scope: Scope,
103 source: LogSource,
104 committed: Prefix,
105 voters: u8,
106}
107
108impl SelectedView {
109 pub const fn scope(self) -> Scope {
111 self.scope
112 }
113 pub const fn source(self) -> LogSource {
115 self.source
116 }
117 pub const fn committed(self) -> Prefix {
119 self.committed
120 }
121 pub const fn voter_mask(self) -> u8 {
123 self.voters
124 }
125}
126
127#[derive(Debug)]
135pub struct ViewChange {
136 pub(crate) normal: NormalReplica,
137 pub(crate) configuration: Configuration,
138 pub(crate) local: usize,
139 view: u64,
140 promised: u64,
141 pending: Option<PromiseTicket>,
142 pub(crate) completed: Option<PromiseTicket>,
143 starts: u8,
144 reports: [Option<DoViewChange>; 3],
145 pub(crate) selected: Option<SelectedView>,
146}
147
148impl ViewChange {
149 pub fn history_sources(&self) -> [Option<LogSource>; 3] {
152 std::array::from_fn(|index| {
153 self.reports[index].map(|report| LogSource {
154 voter: self.configuration.voters()[index],
155 generation: report.generation,
156 accepted: report.log.accepted,
157 })
158 })
159 }
160
161 pub(crate) fn after_abandon(
162 mut normal: NormalReplica,
163 previous: PromiseTicket,
164 requested_view: u64,
165 ) -> Result<Self, ViewChangeError> {
166 normal.restore_fenced(previous.generation(), previous.log());
167 let mut changing = normal.into_view_change(requested_view)?;
168 changing.promised = previous.scope().view;
169 changing.completed = Some(previous);
170 Ok(changing)
171 }
172
173 pub fn recover_intact(
187 configuration: Configuration,
188 local: NodeId,
189 generation: JournalGeneration,
190 recovered: RecoveredState,
191 limits: PipelineLimits,
192 ) -> Result<Self, ViewChangeError> {
193 if configuration.policy() == crate::QuorumPolicy::Replicated {
194 return Err(ViewChangeError::VolatileRestart);
195 }
196 Self::recover_verified(configuration, local, generation, recovered, limits)
197 }
198
199 pub fn recover_drained(
207 configuration: Configuration,
208 local: NodeId,
209 generation: JournalGeneration,
210 recovered: RecoveredState,
211 limits: PipelineLimits,
212 ) -> Result<Self, ViewChangeError> {
213 if configuration.policy() != crate::QuorumPolicy::Replicated {
214 return Err(ReplicationError::PolicyMismatch.into());
215 }
216 Self::recover_verified(configuration, local, generation, recovered, limits)
217 }
218
219 fn recover_verified(
220 configuration: Configuration,
221 local: NodeId,
222 generation: JournalGeneration,
223 recovered: RecoveredState,
224 limits: PipelineLimits,
225 ) -> Result<Self, ViewChangeError> {
226 if recovered.scope
227 != (Scope {
228 view: recovered.scope.view,
229 ..configuration.scope()
230 })
231 {
232 return Err(ReplicationError::ScopeMismatch.into());
233 }
234 if recovered.log.last_normal_view > recovered.scope.view {
235 return Err(ViewChangeError::InvalidReport);
236 }
237 let view = if recovered.scope.view > recovered.log.last_normal_view {
238 recovered.scope.view
239 } else {
240 recovered
241 .scope
242 .view
243 .checked_add(1)
244 .ok_or(ViewChangeError::ViewExhausted)?
245 };
246 validate_report(DoViewChange {
247 scope: Scope {
248 view,
249 ..recovered.scope
250 },
251 generation,
252 log: recovered.log,
253 })?;
254 let normal =
255 NormalReplica::recover_fenced(configuration, local, generation, recovered.log, limits)?;
256 let mut changing = normal.into_view_change(view)?;
257 changing.promised = recovered.scope.view;
258 if changing.promised == view {
259 changing.starts |= 1 << changing.local; changing.completed = Some(PromiseTicket {
261 scope: changing.scope(),
262 generation,
263 log: recovered.log,
264 });
265 }
266 Ok(changing)
267 }
268
269 pub(crate) fn begin(
270 mut normal: NormalReplica,
271 configuration: Configuration,
272 local: usize,
273 view: u64,
274 ) -> Result<Self, ViewChangeError> {
275 normal.fence();
276 if normal.snapshot().status == Status::Faulted {
277 return Err(ViewChangeError::Faulted);
278 }
279 if view <= configuration.scope().view {
280 return Err(ViewChangeError::ViewNotHigher);
281 }
282 Ok(Self {
283 normal,
284 configuration,
285 local,
286 view,
287 promised: configuration.scope().view,
288 pending: None,
289 completed: None,
290 starts: 0,
291 reports: [None; 3],
292 selected: None,
293 })
294 }
295
296 pub fn normal_snapshot(&self) -> ReplicaSnapshot {
298 self.normal.snapshot()
299 }
300
301 pub fn scope(&self) -> Scope {
303 Scope {
304 view: self.view,
305 ..self.configuration.scope()
306 }
307 }
308
309 pub const fn promised_view(&self) -> u64 {
311 self.promised
312 }
313
314 pub fn advance_view(&mut self, view: u64) -> Result<(), ViewChangeError> {
320 self.require_active()?;
321 if view <= self.view {
322 return Err(ViewChangeError::ViewNotHigher);
323 }
324 self.view = view;
325 self.starts = 0;
326 self.reports = [None; 3];
327 self.selected = None;
328 Ok(())
329 }
330
331 pub fn complete_write(&mut self, ticket: WriteTicket) -> Result<(), ViewChangeError> {
333 Ok(self.normal.complete_write(ticket)?)
334 }
335
336 pub fn begin_sync(&self) -> Result<SyncTicket, ViewChangeError> {
338 Ok(self.normal.begin_sync()?)
339 }
340
341 pub fn complete_sync(&mut self, ticket: SyncTicket) -> Result<(), ViewChangeError> {
343 Ok(self.normal.complete_sync(ticket)?)
344 }
345
346 pub fn fail_io(&mut self, generation: JournalGeneration) -> Result<(), ViewChangeError> {
348 Ok(self.normal.fail_io(generation)?)
349 }
350
351 pub fn begin_promise(&mut self) -> Result<PromiseTicket, ViewChangeError> {
356 self.require_active()?;
357 if self.pending.is_some() {
358 return Err(ViewChangeError::PromisePending);
359 }
360 if self.promised == self.view {
361 return Err(ViewChangeError::AlreadyPromised);
362 }
363 let snapshot = self.normal.snapshot();
364 if snapshot.journal.durable != snapshot.accepted.op {
365 return Err(ViewChangeError::StoragePending);
366 }
367 let ticket = PromiseTicket {
368 scope: self.scope(),
369 generation: snapshot.journal.generation,
370 log: FrozenLog {
371 last_normal_view: snapshot.scope.view,
372 accepted: snapshot.accepted,
373 committed: snapshot.committed,
374 },
375 };
376 self.pending = Some(ticket);
377 Ok(ticket)
378 }
379
380 pub fn complete_promise(&mut self, ticket: PromiseTicket) -> Result<(), ViewChangeError> {
382 self.require_active()?;
383 if self.completed == Some(ticket) {
384 return Ok(());
385 }
386 self.require_ticket(ticket)?;
387 self.pending = None;
388 self.completed = Some(ticket);
389 self.promised = ticket.scope.view;
390 if self.promised == self.view {
391 self.starts |= 1 << self.local;
392 }
393 Ok(())
394 }
395
396 pub fn fail_promise(&mut self, ticket: PromiseTicket) -> Result<(), ViewChangeError> {
398 self.require_active()?;
399 self.require_ticket(ticket)?;
400 self.fail_io(ticket.generation)
401 }
402
403 pub fn start_message(&self) -> Result<StartViewChange, ViewChangeError> {
405 self.require_promised()?;
406 Ok(StartViewChange {
407 scope: self.scope(),
408 })
409 }
410
411 pub fn receive_start(
414 &mut self,
415 from: NodeId,
416 message: StartViewChange,
417 ) -> Result<(), ViewChangeError> {
418 self.require_scope(message.scope)?;
419 let voter = self.configuration.voter_index(from)?;
420 if voter == self.local {
421 return Err(ReplicationError::WrongRole.into());
422 }
423 self.starts |= 1 << voter;
424 Ok(())
425 }
426
427 pub fn report(&mut self) -> Result<DoViewChange, ViewChangeError> {
430 self.require_promised()?;
431 if self.starts.count_ones() < 2 {
432 return Err(ViewChangeError::StartQuorumMissing);
433 }
434 let promise = self.completed.expect("requested view promised");
435 let report = DoViewChange {
436 scope: self.scope(),
437 generation: promise.generation,
438 log: promise.log,
439 };
440 self.reports[self.local] = Some(report);
441 Ok(report)
442 }
443
444 pub fn receive_report(
452 &mut self,
453 from: NodeId,
454 report: DoViewChange,
455 ) -> Result<(), ViewChangeError> {
456 self.require_scope(report.scope)?;
457 self.require_primary()?;
458 let voter = self.configuration.voter_index(from)?;
459 if voter == self.local {
460 return Err(ReplicationError::WrongRole.into());
461 }
462 validate_report(report)?;
463 if let Some(previous) = self.reports[voter] {
464 if previous.log != report.log {
465 return Err(self.conflict());
466 }
467 return Ok(());
472 }
473 if self.selected.is_some() {
474 return Err(ViewChangeError::SelectionFrozen);
475 }
476 self.reports[voter] = Some(report);
477 Ok(())
478 }
479
480 pub fn select(
492 &mut self,
493 mut lookup: impl FnMut(LogSource, OpNumber) -> Option<Digest>,
494 ) -> Result<SelectedView, ViewChangeError> {
495 self.require_promised()?;
496 self.require_primary()?;
497 if let Some(selected) = self.selected {
498 return Ok(selected);
499 }
500 let reports = self.reports;
501 if reports[self.local].is_none() || reports.iter().flatten().count() < 2 {
502 return Err(ViewChangeError::ReportQuorumMissing);
503 }
504 let mut chosen = self.local;
505 let mut voters = 0;
506 for (index, report) in reports.iter().enumerate() {
507 let Some(report) = report else {
508 continue;
509 };
510 voters |= 1 << index;
511 let current = reports[chosen].expect("selected report");
512 let rank = (report.log.last_normal_view, report.log.accepted.op);
513 let best = (current.log.last_normal_view, current.log.accepted.op);
514 if rank == best && report.log.accepted != current.log.accepted {
515 return Err(self.conflict());
516 }
517 if rank > best {
518 chosen = index;
519 }
520 }
521 let report = reports[chosen].expect("selected report");
522 let source = LogSource {
523 voter: self.configuration.voters()[chosen],
524 generation: report.generation,
525 accepted: report.log.accepted,
526 };
527 let mut committed = Prefix::GENESIS;
528 for candidate in reports.iter().flatten() {
529 if candidate.log.committed.op > committed.op {
530 committed = candidate.log.committed;
531 }
532 self.verify_prefix(source, candidate.log.committed, &mut lookup)?;
533 if candidate.log.last_normal_view == report.log.last_normal_view {
534 self.verify_prefix(source, candidate.log.accepted, &mut lookup)?;
535 }
536 }
537 let selected = SelectedView {
538 scope: self.scope(),
539 source,
540 committed,
541 voters,
542 };
543 self.selected = Some(selected);
544 Ok(selected)
545 }
546
547 pub(crate) fn verify_prefix(
548 &mut self,
549 source: LogSource,
550 prefix: Prefix,
551 lookup: &mut impl FnMut(LogSource, OpNumber) -> Option<Digest>,
552 ) -> Result<(), ViewChangeError> {
553 if prefix.op > source.accepted.op {
554 return Err(self.conflict());
555 }
556 let digest = if prefix.op == OpNumber(0) {
557 Digest::ZERO
558 } else if prefix.op == source.accepted.op {
559 source.accepted.digest
560 } else {
561 lookup(source, prefix.op).ok_or(ViewChangeError::HistoryMissing)?
562 };
563 if digest != prefix.digest {
564 return Err(self.conflict());
565 }
566 Ok(())
567 }
568
569 fn require_active(&self) -> Result<(), ViewChangeError> {
570 if self.normal.snapshot().status == Status::Faulted {
571 return Err(ViewChangeError::Faulted);
572 }
573 Ok(())
574 }
575
576 pub(crate) fn require_scope(&self, scope: Scope) -> Result<(), ViewChangeError> {
577 self.require_active()?;
578 if scope != self.scope() {
579 return Err(ReplicationError::ScopeMismatch.into());
580 }
581 Ok(())
582 }
583
584 pub(crate) fn require_primary(&self) -> Result<(), ViewChangeError> {
585 if self.configuration.primary(self.view) != self.configuration.voters()[self.local] {
586 return Err(ReplicationError::WrongRole.into());
587 }
588 Ok(())
589 }
590
591 pub(crate) fn require_promised(&self) -> Result<(), ViewChangeError> {
592 self.require_active()?;
593 if self.promised != self.view {
594 return Err(ViewChangeError::PromiseRequired);
595 }
596 Ok(())
597 }
598
599 fn require_ticket(&self, ticket: PromiseTicket) -> Result<(), ViewChangeError> {
600 if self.pending != Some(ticket) {
601 return Err(ViewChangeError::StalePromise);
602 }
603 Ok(())
604 }
605
606 fn conflict(&mut self) -> ViewChangeError {
607 self.normal
608 .fail_io(self.normal.snapshot().journal.generation)
609 .expect("current generation");
610 ViewChangeError::ConflictingHistory
611 }
612}
613
614pub(crate) fn valid_prefix(prefix: Prefix) -> bool {
615 prefix.op.0 != u64::MAX && ((prefix.op.0 == 0) == (prefix.digest == Digest::ZERO))
616}
617
618fn validate_report(report: DoViewChange) -> Result<(), ViewChangeError> {
619 let log = report.log;
620 if log.last_normal_view >= report.scope.view
621 || !valid_prefix(log.accepted)
622 || !valid_prefix(log.committed)
623 || log.committed.op > log.accepted.op
624 || (log.committed.op == log.accepted.op && log.committed != log.accepted)
625 {
626 return Err(ViewChangeError::InvalidReport);
627 }
628 Ok(())
629}
630
631#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
633pub enum ViewChangeError {
634 #[error("retained-memory voter restart requires fresh recovery or drained-state evidence")]
636 VolatileRestart,
637 #[error(transparent)]
639 Replication(#[from] ReplicationError),
640 #[error("view must increase")]
642 ViewNotHigher,
643 #[error("durable view number exhausted")]
645 ViewExhausted,
646 #[error("old-view journal work has not settled")]
648 StoragePending,
649 #[error("a view promise is already pending")]
651 PromisePending,
652 #[error("requested view already promised")]
654 AlreadyPromised,
655 #[error("stale or foreign view-promise completion")]
657 StalePromise,
658 #[error("requested view promise has not completed")]
660 PromiseRequired,
661 #[error("start-view-change quorum missing")]
663 StartQuorumMissing,
664 #[error("do-view-change quorum missing")]
666 ReportQuorumMissing,
667 #[error("selected lineage evidence missing")]
669 HistoryMissing,
670 #[error("invalid frozen view-change report")]
672 InvalidReport,
673 #[error("conflicting view-change history")]
675 ConflictingHistory,
676 #[error("view selection already frozen")]
678 SelectionFrozen,
679 #[error("view has not been selected")]
681 SelectionRequired,
682 #[error("installation reused the old writer generation")]
684 ReusedGeneration,
685 #[error("stale or foreign installation completion")]
687 StaleInstallation,
688 #[error("installation already completed")]
690 InstallationCompleted,
691 #[error("installed application state is not at the required committed prefix")]
693 ApplicationPending,
694 #[error("view change faulted")]
696 Faulted,
697}