canic_backup/restore/apply/journal/
mod.rs1mod commands;
8mod counts;
9mod receipts;
10mod reports;
11mod types;
12
13pub use commands::{
14 RestoreApplyCommandConfig, RestoreApplyCommandPreview, RestoreApplyRunnerCommand,
15};
16use counts::RestoreApplyJournalStateCounts;
17pub use counts::RestoreApplyOperationKindCounts;
18pub(in crate::restore) use receipts::RestoreApplyCommandOutputPair;
19pub use receipts::{
20 RestoreApplyCommandOutput, RestoreApplyOperationReceipt, RestoreApplyOperationReceiptOutcome,
21};
22pub(in crate::restore) use reports::RestoreApplyJournalReport;
23pub use reports::{
24 RestoreApplyPendingSummary, RestoreApplyProgressSummary, RestoreApplyReportOperation,
25 RestoreApplyReportOutcome,
26};
27pub use types::{
28 RestoreApplyJournalError, RestoreApplyJournalOperation, RestoreApplyOperationKind,
29 RestoreApplyOperationState,
30};
31
32use crate::restore::{RestoreApplyDryRun, RestoreApplyDryRunOperation};
33
34use std::collections::BTreeSet;
35
36use serde::{Deserialize, Serialize};
37
38use types::{
39 restore_apply_blocked_reasons, validate_apply_journal_count, validate_apply_journal_nonempty,
40 validate_apply_journal_sequences, validate_apply_journal_version,
41};
42
43#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
51#[serde(deny_unknown_fields)]
52pub struct RestoreApplyJournal {
53 pub journal_version: u16,
54 pub backup_id: String,
55 pub ready: bool,
56 pub blocked_reasons: Vec<String>,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub backup_root: Option<String>,
59 pub operation_count: usize,
60 pub operation_counts: RestoreApplyOperationKindCounts,
61 pub pending_operations: usize,
62 pub ready_operations: usize,
63 pub blocked_operations: usize,
64 pub completed_operations: usize,
65 pub failed_operations: usize,
66 pub operations: Vec<RestoreApplyJournalOperation>,
67 #[serde(default, skip_serializing_if = "Vec::is_empty")]
68 pub operation_receipts: Vec<RestoreApplyOperationReceipt>,
69}
70
71impl RestoreApplyJournal {
72 #[must_use]
74 pub fn from_dry_run(dry_run: &RestoreApplyDryRun) -> Self {
75 let blocked_reasons = restore_apply_blocked_reasons(dry_run);
76 let initial_state = if blocked_reasons.is_empty() {
77 RestoreApplyOperationState::Ready
78 } else {
79 RestoreApplyOperationState::Blocked
80 };
81 let operations = dry_run
82 .operations
83 .iter()
84 .map(|operation| {
85 RestoreApplyJournalOperation::from_dry_run_operation(
86 operation,
87 initial_state.clone(),
88 &blocked_reasons,
89 )
90 })
91 .collect::<Vec<_>>();
92 let ready_operations = operations
93 .iter()
94 .filter(|operation| operation.state == RestoreApplyOperationState::Ready)
95 .count();
96 let blocked_operations = operations
97 .iter()
98 .filter(|operation| operation.state == RestoreApplyOperationState::Blocked)
99 .count();
100 let operation_counts = RestoreApplyOperationKindCounts::from_operations(&operations);
101
102 Self {
103 journal_version: 1,
104 backup_id: dry_run.backup_id.clone(),
105 ready: blocked_reasons.is_empty(),
106 blocked_reasons,
107 backup_root: dry_run
108 .artifact_validation
109 .as_ref()
110 .map(|validation| validation.backup_root.clone()),
111 operation_count: operations.len(),
112 operation_counts,
113 pending_operations: 0,
114 ready_operations,
115 blocked_operations,
116 completed_operations: 0,
117 failed_operations: 0,
118 operations,
119 operation_receipts: Vec::new(),
120 }
121 }
122
123 pub fn validate(&self) -> Result<(), RestoreApplyJournalError> {
125 validate_apply_journal_version(self.journal_version)?;
126 validate_apply_journal_nonempty("backup_id", &self.backup_id)?;
127 if let Some(backup_root) = &self.backup_root {
128 validate_apply_journal_nonempty("backup_root", backup_root)?;
129 }
130 validate_apply_journal_count(
131 "operation_count",
132 self.operation_count,
133 self.operations.len(),
134 )?;
135
136 let state_counts = RestoreApplyJournalStateCounts::from_operations(&self.operations);
137 let operation_counts = RestoreApplyOperationKindCounts::from_operations(&self.operations);
138 self.operation_counts.validate_matches(&operation_counts)?;
139 validate_apply_journal_count(
140 "pending_operations",
141 self.pending_operations,
142 state_counts.pending,
143 )?;
144 validate_apply_journal_count(
145 "ready_operations",
146 self.ready_operations,
147 state_counts.ready,
148 )?;
149 validate_apply_journal_count(
150 "blocked_operations",
151 self.blocked_operations,
152 state_counts.blocked,
153 )?;
154 validate_apply_journal_count(
155 "completed_operations",
156 self.completed_operations,
157 state_counts.completed,
158 )?;
159 validate_apply_journal_count(
160 "failed_operations",
161 self.failed_operations,
162 state_counts.failed,
163 )?;
164
165 if self.ready && (!self.blocked_reasons.is_empty() || self.blocked_operations > 0) {
166 return Err(RestoreApplyJournalError::ReadyJournalHasBlockingState);
167 }
168
169 validate_apply_journal_sequences(&self.operations)?;
170 for operation in &self.operations {
171 operation.validate()?;
172 }
173 self.validate_operation_receipt_attempts()?;
174 for receipt in &self.operation_receipts {
175 receipt.validate_against(self)?;
176 }
177
178 Ok(())
179 }
180
181 #[must_use]
183 pub(in crate::restore) fn report(&self) -> RestoreApplyJournalReport {
184 RestoreApplyJournalReport::from_journal(self)
185 }
186
187 #[must_use]
189 pub(in crate::restore) fn next_transition_operation(
190 &self,
191 ) -> Option<&RestoreApplyJournalOperation> {
192 self.operations
193 .iter()
194 .filter(|operation| {
195 matches!(
196 operation.state,
197 RestoreApplyOperationState::Ready
198 | RestoreApplyOperationState::Pending
199 | RestoreApplyOperationState::Failed
200 )
201 })
202 .min_by_key(|operation| operation.sequence)
203 }
204
205 #[must_use]
207 pub fn next_command_preview(&self) -> RestoreApplyCommandPreview {
208 RestoreApplyCommandPreview::from_journal(self)
209 }
210
211 #[must_use]
213 pub(in crate::restore) fn next_command_preview_with_config(
214 &self,
215 config: &RestoreApplyCommandConfig,
216 ) -> RestoreApplyCommandPreview {
217 RestoreApplyCommandPreview::from_journal_with_config(self, config)
218 }
219
220 pub(in crate::restore) fn record_operation_receipt(
222 &mut self,
223 receipt: RestoreApplyOperationReceipt,
224 ) -> Result<(), RestoreApplyJournalError> {
225 self.operation_receipts.push(receipt);
226 if let Err(error) = self.validate() {
227 self.operation_receipts.pop();
228 return Err(error);
229 }
230
231 Ok(())
232 }
233
234 pub fn mark_next_operation_pending_at(
236 &mut self,
237 updated_at: Option<String>,
238 ) -> Result<(), RestoreApplyJournalError> {
239 let sequence = self
240 .next_transition_sequence()
241 .ok_or(RestoreApplyJournalError::NoTransitionableOperation)?;
242 self.mark_operation_pending_at(sequence, updated_at)
243 }
244
245 pub(in crate::restore) fn mark_operation_pending_at(
247 &mut self,
248 sequence: usize,
249 updated_at: Option<String>,
250 ) -> Result<(), RestoreApplyJournalError> {
251 self.transition_operation(
252 sequence,
253 RestoreApplyOperationState::Pending,
254 Vec::new(),
255 updated_at,
256 )
257 }
258
259 pub(in crate::restore) fn mark_next_operation_ready_at(
261 &mut self,
262 updated_at: Option<String>,
263 ) -> Result<(), RestoreApplyJournalError> {
264 let operation = self
265 .next_transition_operation()
266 .ok_or(RestoreApplyJournalError::NoTransitionableOperation)?;
267 if operation.state != RestoreApplyOperationState::Pending {
268 return Err(RestoreApplyJournalError::NoPendingOperation);
269 }
270
271 self.mark_operation_ready_at(operation.sequence, updated_at)
272 }
273
274 pub(in crate::restore) fn mark_operation_ready_at(
276 &mut self,
277 sequence: usize,
278 updated_at: Option<String>,
279 ) -> Result<(), RestoreApplyJournalError> {
280 self.transition_operation(
281 sequence,
282 RestoreApplyOperationState::Ready,
283 Vec::new(),
284 updated_at,
285 )
286 }
287
288 pub fn retry_failed_operation_at(
290 &mut self,
291 sequence: usize,
292 updated_at: Option<String>,
293 ) -> Result<(), RestoreApplyJournalError> {
294 self.transition_operation(
295 sequence,
296 RestoreApplyOperationState::Ready,
297 Vec::new(),
298 updated_at,
299 )
300 }
301
302 pub(in crate::restore) fn mark_operation_completed_at(
304 &mut self,
305 sequence: usize,
306 updated_at: Option<String>,
307 ) -> Result<(), RestoreApplyJournalError> {
308 self.transition_operation(
309 sequence,
310 RestoreApplyOperationState::Completed,
311 Vec::new(),
312 updated_at,
313 )
314 }
315
316 pub(in crate::restore) fn mark_operation_failed_at(
318 &mut self,
319 sequence: usize,
320 reason: String,
321 updated_at: Option<String>,
322 ) -> Result<(), RestoreApplyJournalError> {
323 if reason.trim().is_empty() {
324 return Err(RestoreApplyJournalError::FailureReasonRequired(sequence));
325 }
326
327 self.transition_operation(
328 sequence,
329 RestoreApplyOperationState::Failed,
330 vec![reason],
331 updated_at,
332 )
333 }
334
335 fn transition_operation(
337 &mut self,
338 sequence: usize,
339 next_state: RestoreApplyOperationState,
340 blocking_reasons: Vec<String>,
341 updated_at: Option<String>,
342 ) -> Result<(), RestoreApplyJournalError> {
343 let index = self
344 .operations
345 .iter()
346 .position(|operation| operation.sequence == sequence)
347 .ok_or(RestoreApplyJournalError::OperationNotFound(sequence))?;
348 let operation = &self.operations[index];
349
350 if !operation.can_transition_to(&next_state) {
351 return Err(RestoreApplyJournalError::InvalidOperationTransition {
352 sequence,
353 from: operation.state.clone(),
354 to: next_state,
355 });
356 }
357
358 self.validate_operation_transition_order(operation, &next_state)?;
359
360 let operation = &mut self.operations[index];
361 operation.state = next_state;
362 operation.blocking_reasons = blocking_reasons;
363 operation.state_updated_at = updated_at;
364 self.refresh_operation_counts();
365 self.validate()
366 }
367
368 fn validate_operation_transition_order(
370 &self,
371 operation: &RestoreApplyJournalOperation,
372 next_state: &RestoreApplyOperationState,
373 ) -> Result<(), RestoreApplyJournalError> {
374 if operation.state == *next_state {
375 return Ok(());
376 }
377
378 let next_sequence = self
379 .next_transition_sequence()
380 .ok_or(RestoreApplyJournalError::NoTransitionableOperation)?;
381
382 if operation.sequence == next_sequence {
383 return Ok(());
384 }
385
386 Err(RestoreApplyJournalError::OutOfOrderOperationTransition {
387 requested: operation.sequence,
388 next: next_sequence,
389 })
390 }
391
392 fn next_transition_sequence(&self) -> Option<usize> {
394 self.next_transition_operation()
395 .map(|operation| operation.sequence)
396 }
397
398 fn refresh_operation_counts(&mut self) {
400 let state_counts = RestoreApplyJournalStateCounts::from_operations(&self.operations);
401 self.operation_count = self.operations.len();
402 self.operation_counts = RestoreApplyOperationKindCounts::from_operations(&self.operations);
403 self.pending_operations = state_counts.pending;
404 self.ready_operations = state_counts.ready;
405 self.blocked_operations = state_counts.blocked;
406 self.completed_operations = state_counts.completed;
407 self.failed_operations = state_counts.failed;
408 }
409
410 pub(super) const fn is_complete(&self) -> bool {
412 self.operation_count > 0 && self.completed_operations == self.operation_count
413 }
414
415 pub(super) fn operation_kind_counts(&self) -> RestoreApplyOperationKindCounts {
417 RestoreApplyOperationKindCounts::from_operations(&self.operations)
418 }
419
420 fn validate_operation_receipt_attempts(&self) -> Result<(), RestoreApplyJournalError> {
422 let mut attempts = BTreeSet::new();
423 for receipt in &self.operation_receipts {
424 if !attempts.insert((receipt.sequence, receipt.attempt)) {
425 return Err(RestoreApplyJournalError::DuplicateOperationReceiptAttempt {
426 sequence: receipt.sequence,
427 attempt: receipt.attempt,
428 });
429 }
430 }
431
432 Ok(())
433 }
434
435 pub(super) fn uploaded_snapshot_id_for_load(
437 &self,
438 load: &RestoreApplyJournalOperation,
439 ) -> Option<&str> {
440 self.operation_receipts
441 .iter()
442 .find(|receipt| {
443 receipt.matches_load_operation(load)
444 && self.operations.iter().any(|operation| {
445 operation.sequence == receipt.sequence
446 && operation.operation == RestoreApplyOperationKind::UploadSnapshot
447 && operation.state == RestoreApplyOperationState::Completed
448 })
449 })
450 .and_then(|receipt| receipt.uploaded_snapshot_id.as_deref())
451 }
452}