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(deserialize_with = "crate::serialization::required_option")]
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 pub operation_receipts: Vec<RestoreApplyOperationReceipt>,
68}
69
70impl RestoreApplyJournal {
71 pub fn from_dry_run(dry_run: &RestoreApplyDryRun) -> Result<Self, RestoreApplyJournalError> {
73 dry_run.validate()?;
74 let blocked_reasons = restore_apply_blocked_reasons(dry_run);
75 let initial_state = if blocked_reasons.is_empty() {
76 RestoreApplyOperationState::Ready
77 } else {
78 RestoreApplyOperationState::Blocked
79 };
80 let operations = dry_run
81 .operations
82 .iter()
83 .map(|operation| {
84 RestoreApplyJournalOperation::from_dry_run_operation(
85 operation,
86 initial_state.clone(),
87 &blocked_reasons,
88 )
89 })
90 .collect::<Vec<_>>();
91 let ready_operations = operations
92 .iter()
93 .filter(|operation| operation.state == RestoreApplyOperationState::Ready)
94 .count();
95 let blocked_operations = operations
96 .iter()
97 .filter(|operation| operation.state == RestoreApplyOperationState::Blocked)
98 .count();
99 let operation_counts = RestoreApplyOperationKindCounts::from_operations(&operations);
100
101 let journal = Self {
102 journal_version: 1,
103 backup_id: dry_run.backup_id.clone(),
104 ready: blocked_reasons.is_empty(),
105 blocked_reasons,
106 backup_root: dry_run
107 .artifact_validation
108 .as_ref()
109 .map(|validation| validation.backup_root.clone()),
110 operation_count: operations.len(),
111 operation_counts,
112 pending_operations: 0,
113 ready_operations,
114 blocked_operations,
115 completed_operations: 0,
116 failed_operations: 0,
117 operations,
118 operation_receipts: Vec::new(),
119 };
120 journal.validate()?;
121 Ok(journal)
122 }
123
124 pub fn validate(&self) -> Result<(), RestoreApplyJournalError> {
126 validate_apply_journal_version(self.journal_version)?;
127 validate_apply_journal_nonempty("backup_id", &self.backup_id)?;
128 if let Some(backup_root) = &self.backup_root {
129 validate_apply_journal_nonempty("backup_root", backup_root)?;
130 }
131 validate_apply_journal_count(
132 "operation_count",
133 self.operation_count,
134 self.operations.len(),
135 )?;
136
137 let state_counts = RestoreApplyJournalStateCounts::from_operations(&self.operations);
138 let operation_counts = RestoreApplyOperationKindCounts::from_operations(&self.operations);
139 self.operation_counts.validate_matches(&operation_counts)?;
140 validate_apply_journal_count(
141 "pending_operations",
142 self.pending_operations,
143 state_counts.pending,
144 )?;
145 validate_apply_journal_count(
146 "ready_operations",
147 self.ready_operations,
148 state_counts.ready,
149 )?;
150 validate_apply_journal_count(
151 "blocked_operations",
152 self.blocked_operations,
153 state_counts.blocked,
154 )?;
155 validate_apply_journal_count(
156 "completed_operations",
157 self.completed_operations,
158 state_counts.completed,
159 )?;
160 validate_apply_journal_count(
161 "failed_operations",
162 self.failed_operations,
163 state_counts.failed,
164 )?;
165
166 if self.ready && (!self.blocked_reasons.is_empty() || self.blocked_operations > 0) {
167 return Err(RestoreApplyJournalError::ReadyJournalHasBlockingState);
168 }
169
170 validate_apply_journal_sequences(&self.operations)?;
171 for operation in &self.operations {
172 operation.validate()?;
173 }
174 self.validate_operation_receipt_attempts()?;
175 for receipt in &self.operation_receipts {
176 receipt.validate_against(self)?;
177 }
178
179 Ok(())
180 }
181
182 #[must_use]
184 pub(in crate::restore) fn report(&self) -> RestoreApplyJournalReport {
185 RestoreApplyJournalReport::from_journal(self)
186 }
187
188 #[must_use]
190 pub(in crate::restore) fn next_transition_operation(
191 &self,
192 ) -> Option<&RestoreApplyJournalOperation> {
193 self.operations
194 .iter()
195 .filter(|operation| {
196 matches!(
197 operation.state,
198 RestoreApplyOperationState::Ready
199 | RestoreApplyOperationState::Pending
200 | RestoreApplyOperationState::Failed
201 )
202 })
203 .min_by_key(|operation| operation.sequence)
204 }
205
206 #[must_use]
208 pub(in crate::restore) fn next_command_preview_with_config(
209 &self,
210 config: &RestoreApplyCommandConfig,
211 ) -> RestoreApplyCommandPreview {
212 RestoreApplyCommandPreview::from_journal_with_config(self, config)
213 }
214
215 pub(in crate::restore) fn record_operation_receipt(
217 &mut self,
218 receipt: RestoreApplyOperationReceipt,
219 ) -> Result<(), RestoreApplyJournalError> {
220 self.operation_receipts.push(receipt);
221 if let Err(error) = self.validate() {
222 self.operation_receipts.pop();
223 return Err(error);
224 }
225
226 Ok(())
227 }
228
229 pub fn mark_next_operation_pending_at(
231 &mut self,
232 updated_at: Option<String>,
233 ) -> Result<(), RestoreApplyJournalError> {
234 let sequence = self
235 .next_transition_sequence()
236 .ok_or(RestoreApplyJournalError::NoTransitionableOperation)?;
237 self.mark_operation_pending_at(sequence, updated_at)
238 }
239
240 pub(in crate::restore) fn mark_operation_pending_at(
242 &mut self,
243 sequence: usize,
244 updated_at: Option<String>,
245 ) -> Result<(), RestoreApplyJournalError> {
246 self.transition_operation(
247 sequence,
248 RestoreApplyOperationState::Pending,
249 Vec::new(),
250 updated_at,
251 )
252 }
253
254 pub(in crate::restore) fn mark_upload_snapshot_pending_at(
256 &mut self,
257 sequence: usize,
258 updated_at: Option<String>,
259 snapshot_ids_before: Vec<String>,
260 ) -> Result<(), RestoreApplyJournalError> {
261 let index = self
262 .operations
263 .iter()
264 .position(|operation| operation.sequence == sequence)
265 .ok_or(RestoreApplyJournalError::OperationNotFound(sequence))?;
266 if self.operations[index].operation != RestoreApplyOperationKind::UploadSnapshot {
267 return Err(RestoreApplyJournalError::UnexpectedSnapshotInventory(
268 sequence,
269 ));
270 }
271 let previous = self.operations[index].snapshot_ids_before.clone();
272 self.operations[index].snapshot_ids_before = Some(snapshot_ids_before);
273 if let Err(error) = self.transition_operation(
274 sequence,
275 RestoreApplyOperationState::Pending,
276 Vec::new(),
277 updated_at,
278 ) {
279 self.operations[index].snapshot_ids_before = previous;
280 return Err(error);
281 }
282 Ok(())
283 }
284
285 pub fn retry_failed_operation_at(
287 &mut self,
288 sequence: usize,
289 updated_at: Option<String>,
290 ) -> Result<(), RestoreApplyJournalError> {
291 self.transition_operation(
292 sequence,
293 RestoreApplyOperationState::Ready,
294 Vec::new(),
295 updated_at,
296 )
297 }
298
299 pub(in crate::restore) fn mark_operation_completed_at(
301 &mut self,
302 sequence: usize,
303 updated_at: Option<String>,
304 ) -> Result<(), RestoreApplyJournalError> {
305 self.transition_operation(
306 sequence,
307 RestoreApplyOperationState::Completed,
308 Vec::new(),
309 updated_at,
310 )
311 }
312
313 pub(in crate::restore) fn mark_operation_failed_at(
315 &mut self,
316 sequence: usize,
317 reason: String,
318 updated_at: Option<String>,
319 ) -> Result<(), RestoreApplyJournalError> {
320 if reason.trim().is_empty() {
321 return Err(RestoreApplyJournalError::FailureReasonRequired(sequence));
322 }
323
324 self.transition_operation(
325 sequence,
326 RestoreApplyOperationState::Failed,
327 vec![reason],
328 updated_at,
329 )
330 }
331
332 fn transition_operation(
334 &mut self,
335 sequence: usize,
336 next_state: RestoreApplyOperationState,
337 blocking_reasons: Vec<String>,
338 updated_at: Option<String>,
339 ) -> Result<(), RestoreApplyJournalError> {
340 let index = self
341 .operations
342 .iter()
343 .position(|operation| operation.sequence == sequence)
344 .ok_or(RestoreApplyJournalError::OperationNotFound(sequence))?;
345 let operation = &self.operations[index];
346
347 if !operation.can_transition_to(&next_state) {
348 return Err(RestoreApplyJournalError::InvalidOperationTransition {
349 sequence,
350 from: operation.state.clone(),
351 to: next_state,
352 });
353 }
354
355 self.validate_operation_transition_order(operation, &next_state)?;
356
357 let operation = &mut self.operations[index];
358 operation.state = next_state;
359 operation.blocking_reasons = blocking_reasons;
360 operation.state_updated_at = updated_at;
361 self.refresh_operation_counts();
362 self.validate()
363 }
364
365 fn validate_operation_transition_order(
367 &self,
368 operation: &RestoreApplyJournalOperation,
369 next_state: &RestoreApplyOperationState,
370 ) -> Result<(), RestoreApplyJournalError> {
371 if operation.state == *next_state {
372 return Ok(());
373 }
374
375 let next_sequence = self
376 .next_transition_sequence()
377 .ok_or(RestoreApplyJournalError::NoTransitionableOperation)?;
378
379 if operation.sequence == next_sequence {
380 return Ok(());
381 }
382
383 Err(RestoreApplyJournalError::OutOfOrderOperationTransition {
384 requested: operation.sequence,
385 next: next_sequence,
386 })
387 }
388
389 fn next_transition_sequence(&self) -> Option<usize> {
391 self.next_transition_operation()
392 .map(|operation| operation.sequence)
393 }
394
395 fn refresh_operation_counts(&mut self) {
397 let state_counts = RestoreApplyJournalStateCounts::from_operations(&self.operations);
398 self.operation_count = self.operations.len();
399 self.operation_counts = RestoreApplyOperationKindCounts::from_operations(&self.operations);
400 self.pending_operations = state_counts.pending;
401 self.ready_operations = state_counts.ready;
402 self.blocked_operations = state_counts.blocked;
403 self.completed_operations = state_counts.completed;
404 self.failed_operations = state_counts.failed;
405 }
406
407 pub(super) const fn is_complete(&self) -> bool {
409 self.operation_count > 0 && self.completed_operations == self.operation_count
410 }
411
412 pub(super) fn operation_kind_counts(&self) -> RestoreApplyOperationKindCounts {
414 RestoreApplyOperationKindCounts::from_operations(&self.operations)
415 }
416
417 fn validate_operation_receipt_attempts(&self) -> Result<(), RestoreApplyJournalError> {
419 let mut attempts = BTreeSet::new();
420 for receipt in &self.operation_receipts {
421 if receipt.attempt == 0 {
422 return Err(RestoreApplyJournalError::InvalidOperationReceiptAttempt {
423 sequence: receipt.sequence,
424 attempt: receipt.attempt,
425 });
426 }
427 if !attempts.insert((receipt.sequence, receipt.attempt)) {
428 return Err(RestoreApplyJournalError::DuplicateOperationReceiptAttempt {
429 sequence: receipt.sequence,
430 attempt: receipt.attempt,
431 });
432 }
433 }
434
435 Ok(())
436 }
437
438 pub(super) fn uploaded_snapshot_id_for_load(
440 &self,
441 load: &RestoreApplyJournalOperation,
442 ) -> Option<&str> {
443 self.operation_receipts
444 .iter()
445 .find(|receipt| {
446 receipt.matches_load_operation(load)
447 && self.operations.iter().any(|operation| {
448 operation.sequence == receipt.sequence
449 && operation.operation == RestoreApplyOperationKind::UploadSnapshot
450 && operation.state == RestoreApplyOperationState::Completed
451 })
452 })
453 .and_then(|receipt| receipt.uploaded_snapshot_id.as_deref())
454 }
455}