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 fn next_command_preview(&self) -> RestoreApplyCommandPreview {
209 RestoreApplyCommandPreview::from_journal(self)
210 }
211
212 #[must_use]
214 pub(in crate::restore) fn next_command_preview_with_config(
215 &self,
216 config: &RestoreApplyCommandConfig,
217 ) -> RestoreApplyCommandPreview {
218 RestoreApplyCommandPreview::from_journal_with_config(self, config)
219 }
220
221 pub(in crate::restore) fn record_operation_receipt(
223 &mut self,
224 receipt: RestoreApplyOperationReceipt,
225 ) -> Result<(), RestoreApplyJournalError> {
226 self.operation_receipts.push(receipt);
227 if let Err(error) = self.validate() {
228 self.operation_receipts.pop();
229 return Err(error);
230 }
231
232 Ok(())
233 }
234
235 pub fn mark_next_operation_pending_at(
237 &mut self,
238 updated_at: Option<String>,
239 ) -> Result<(), RestoreApplyJournalError> {
240 let sequence = self
241 .next_transition_sequence()
242 .ok_or(RestoreApplyJournalError::NoTransitionableOperation)?;
243 self.mark_operation_pending_at(sequence, updated_at)
244 }
245
246 pub(in crate::restore) fn mark_operation_pending_at(
248 &mut self,
249 sequence: usize,
250 updated_at: Option<String>,
251 ) -> Result<(), RestoreApplyJournalError> {
252 self.transition_operation(
253 sequence,
254 RestoreApplyOperationState::Pending,
255 Vec::new(),
256 updated_at,
257 )
258 }
259
260 pub(in crate::restore) fn mark_upload_snapshot_pending_at(
262 &mut self,
263 sequence: usize,
264 updated_at: Option<String>,
265 snapshot_ids_before: Vec<String>,
266 ) -> Result<(), RestoreApplyJournalError> {
267 let index = self
268 .operations
269 .iter()
270 .position(|operation| operation.sequence == sequence)
271 .ok_or(RestoreApplyJournalError::OperationNotFound(sequence))?;
272 if self.operations[index].operation != RestoreApplyOperationKind::UploadSnapshot {
273 return Err(RestoreApplyJournalError::UnexpectedSnapshotInventory(
274 sequence,
275 ));
276 }
277 let previous = self.operations[index].snapshot_ids_before.clone();
278 self.operations[index].snapshot_ids_before = Some(snapshot_ids_before);
279 if let Err(error) = self.transition_operation(
280 sequence,
281 RestoreApplyOperationState::Pending,
282 Vec::new(),
283 updated_at,
284 ) {
285 self.operations[index].snapshot_ids_before = previous;
286 return Err(error);
287 }
288 Ok(())
289 }
290
291 pub fn retry_failed_operation_at(
293 &mut self,
294 sequence: usize,
295 updated_at: Option<String>,
296 ) -> Result<(), RestoreApplyJournalError> {
297 self.transition_operation(
298 sequence,
299 RestoreApplyOperationState::Ready,
300 Vec::new(),
301 updated_at,
302 )
303 }
304
305 pub(in crate::restore) fn mark_operation_completed_at(
307 &mut self,
308 sequence: usize,
309 updated_at: Option<String>,
310 ) -> Result<(), RestoreApplyJournalError> {
311 self.transition_operation(
312 sequence,
313 RestoreApplyOperationState::Completed,
314 Vec::new(),
315 updated_at,
316 )
317 }
318
319 pub(in crate::restore) fn mark_operation_failed_at(
321 &mut self,
322 sequence: usize,
323 reason: String,
324 updated_at: Option<String>,
325 ) -> Result<(), RestoreApplyJournalError> {
326 if reason.trim().is_empty() {
327 return Err(RestoreApplyJournalError::FailureReasonRequired(sequence));
328 }
329
330 self.transition_operation(
331 sequence,
332 RestoreApplyOperationState::Failed,
333 vec![reason],
334 updated_at,
335 )
336 }
337
338 fn transition_operation(
340 &mut self,
341 sequence: usize,
342 next_state: RestoreApplyOperationState,
343 blocking_reasons: Vec<String>,
344 updated_at: Option<String>,
345 ) -> Result<(), RestoreApplyJournalError> {
346 let index = self
347 .operations
348 .iter()
349 .position(|operation| operation.sequence == sequence)
350 .ok_or(RestoreApplyJournalError::OperationNotFound(sequence))?;
351 let operation = &self.operations[index];
352
353 if !operation.can_transition_to(&next_state) {
354 return Err(RestoreApplyJournalError::InvalidOperationTransition {
355 sequence,
356 from: operation.state.clone(),
357 to: next_state,
358 });
359 }
360
361 self.validate_operation_transition_order(operation, &next_state)?;
362
363 let operation = &mut self.operations[index];
364 operation.state = next_state;
365 operation.blocking_reasons = blocking_reasons;
366 operation.state_updated_at = updated_at;
367 self.refresh_operation_counts();
368 self.validate()
369 }
370
371 fn validate_operation_transition_order(
373 &self,
374 operation: &RestoreApplyJournalOperation,
375 next_state: &RestoreApplyOperationState,
376 ) -> Result<(), RestoreApplyJournalError> {
377 if operation.state == *next_state {
378 return Ok(());
379 }
380
381 let next_sequence = self
382 .next_transition_sequence()
383 .ok_or(RestoreApplyJournalError::NoTransitionableOperation)?;
384
385 if operation.sequence == next_sequence {
386 return Ok(());
387 }
388
389 Err(RestoreApplyJournalError::OutOfOrderOperationTransition {
390 requested: operation.sequence,
391 next: next_sequence,
392 })
393 }
394
395 fn next_transition_sequence(&self) -> Option<usize> {
397 self.next_transition_operation()
398 .map(|operation| operation.sequence)
399 }
400
401 fn refresh_operation_counts(&mut self) {
403 let state_counts = RestoreApplyJournalStateCounts::from_operations(&self.operations);
404 self.operation_count = self.operations.len();
405 self.operation_counts = RestoreApplyOperationKindCounts::from_operations(&self.operations);
406 self.pending_operations = state_counts.pending;
407 self.ready_operations = state_counts.ready;
408 self.blocked_operations = state_counts.blocked;
409 self.completed_operations = state_counts.completed;
410 self.failed_operations = state_counts.failed;
411 }
412
413 pub(super) const fn is_complete(&self) -> bool {
415 self.operation_count > 0 && self.completed_operations == self.operation_count
416 }
417
418 pub(super) fn operation_kind_counts(&self) -> RestoreApplyOperationKindCounts {
420 RestoreApplyOperationKindCounts::from_operations(&self.operations)
421 }
422
423 fn validate_operation_receipt_attempts(&self) -> Result<(), RestoreApplyJournalError> {
425 let mut attempts = BTreeSet::new();
426 for receipt in &self.operation_receipts {
427 if receipt.attempt == 0 {
428 return Err(RestoreApplyJournalError::InvalidOperationReceiptAttempt {
429 sequence: receipt.sequence,
430 attempt: receipt.attempt,
431 });
432 }
433 if !attempts.insert((receipt.sequence, receipt.attempt)) {
434 return Err(RestoreApplyJournalError::DuplicateOperationReceiptAttempt {
435 sequence: receipt.sequence,
436 attempt: receipt.attempt,
437 });
438 }
439 }
440
441 Ok(())
442 }
443
444 pub(super) fn uploaded_snapshot_id_for_load(
446 &self,
447 load: &RestoreApplyJournalOperation,
448 ) -> Option<&str> {
449 self.operation_receipts
450 .iter()
451 .find(|receipt| {
452 receipt.matches_load_operation(load)
453 && self.operations.iter().any(|operation| {
454 operation.sequence == receipt.sequence
455 && operation.operation == RestoreApplyOperationKind::UploadSnapshot
456 && operation.state == RestoreApplyOperationState::Completed
457 })
458 })
459 .and_then(|receipt| receipt.uploaded_snapshot_id.as_deref())
460 }
461}