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_next_operation_ready_at(
262 &mut self,
263 updated_at: Option<String>,
264 ) -> Result<(), RestoreApplyJournalError> {
265 let operation = self
266 .next_transition_operation()
267 .ok_or(RestoreApplyJournalError::NoTransitionableOperation)?;
268 if operation.state != RestoreApplyOperationState::Pending {
269 return Err(RestoreApplyJournalError::NoPendingOperation);
270 }
271
272 self.mark_operation_ready_at(operation.sequence, updated_at)
273 }
274
275 pub(in crate::restore) fn mark_operation_ready_at(
277 &mut self,
278 sequence: usize,
279 updated_at: Option<String>,
280 ) -> Result<(), RestoreApplyJournalError> {
281 self.transition_operation(
282 sequence,
283 RestoreApplyOperationState::Ready,
284 Vec::new(),
285 updated_at,
286 )
287 }
288
289 pub fn retry_failed_operation_at(
291 &mut self,
292 sequence: usize,
293 updated_at: Option<String>,
294 ) -> Result<(), RestoreApplyJournalError> {
295 self.transition_operation(
296 sequence,
297 RestoreApplyOperationState::Ready,
298 Vec::new(),
299 updated_at,
300 )
301 }
302
303 pub(in crate::restore) fn mark_operation_completed_at(
305 &mut self,
306 sequence: usize,
307 updated_at: Option<String>,
308 ) -> Result<(), RestoreApplyJournalError> {
309 self.transition_operation(
310 sequence,
311 RestoreApplyOperationState::Completed,
312 Vec::new(),
313 updated_at,
314 )
315 }
316
317 pub(in crate::restore) fn mark_operation_failed_at(
319 &mut self,
320 sequence: usize,
321 reason: String,
322 updated_at: Option<String>,
323 ) -> Result<(), RestoreApplyJournalError> {
324 if reason.trim().is_empty() {
325 return Err(RestoreApplyJournalError::FailureReasonRequired(sequence));
326 }
327
328 self.transition_operation(
329 sequence,
330 RestoreApplyOperationState::Failed,
331 vec![reason],
332 updated_at,
333 )
334 }
335
336 fn transition_operation(
338 &mut self,
339 sequence: usize,
340 next_state: RestoreApplyOperationState,
341 blocking_reasons: Vec<String>,
342 updated_at: Option<String>,
343 ) -> Result<(), RestoreApplyJournalError> {
344 let index = self
345 .operations
346 .iter()
347 .position(|operation| operation.sequence == sequence)
348 .ok_or(RestoreApplyJournalError::OperationNotFound(sequence))?;
349 let operation = &self.operations[index];
350
351 if !operation.can_transition_to(&next_state) {
352 return Err(RestoreApplyJournalError::InvalidOperationTransition {
353 sequence,
354 from: operation.state.clone(),
355 to: next_state,
356 });
357 }
358
359 self.validate_operation_transition_order(operation, &next_state)?;
360
361 let operation = &mut self.operations[index];
362 operation.state = next_state;
363 operation.blocking_reasons = blocking_reasons;
364 operation.state_updated_at = updated_at;
365 self.refresh_operation_counts();
366 self.validate()
367 }
368
369 fn validate_operation_transition_order(
371 &self,
372 operation: &RestoreApplyJournalOperation,
373 next_state: &RestoreApplyOperationState,
374 ) -> Result<(), RestoreApplyJournalError> {
375 if operation.state == *next_state {
376 return Ok(());
377 }
378
379 let next_sequence = self
380 .next_transition_sequence()
381 .ok_or(RestoreApplyJournalError::NoTransitionableOperation)?;
382
383 if operation.sequence == next_sequence {
384 return Ok(());
385 }
386
387 Err(RestoreApplyJournalError::OutOfOrderOperationTransition {
388 requested: operation.sequence,
389 next: next_sequence,
390 })
391 }
392
393 fn next_transition_sequence(&self) -> Option<usize> {
395 self.next_transition_operation()
396 .map(|operation| operation.sequence)
397 }
398
399 fn refresh_operation_counts(&mut self) {
401 let state_counts = RestoreApplyJournalStateCounts::from_operations(&self.operations);
402 self.operation_count = self.operations.len();
403 self.operation_counts = RestoreApplyOperationKindCounts::from_operations(&self.operations);
404 self.pending_operations = state_counts.pending;
405 self.ready_operations = state_counts.ready;
406 self.blocked_operations = state_counts.blocked;
407 self.completed_operations = state_counts.completed;
408 self.failed_operations = state_counts.failed;
409 }
410
411 pub(super) const fn is_complete(&self) -> bool {
413 self.operation_count > 0 && self.completed_operations == self.operation_count
414 }
415
416 pub(super) fn operation_kind_counts(&self) -> RestoreApplyOperationKindCounts {
418 RestoreApplyOperationKindCounts::from_operations(&self.operations)
419 }
420
421 fn validate_operation_receipt_attempts(&self) -> Result<(), RestoreApplyJournalError> {
423 let mut attempts = BTreeSet::new();
424 for receipt in &self.operation_receipts {
425 if receipt.attempt == 0 {
426 return Err(RestoreApplyJournalError::InvalidOperationReceiptAttempt {
427 sequence: receipt.sequence,
428 attempt: receipt.attempt,
429 });
430 }
431 if !attempts.insert((receipt.sequence, receipt.attempt)) {
432 return Err(RestoreApplyJournalError::DuplicateOperationReceiptAttempt {
433 sequence: receipt.sequence,
434 attempt: receipt.attempt,
435 });
436 }
437 }
438
439 Ok(())
440 }
441
442 pub(super) fn uploaded_snapshot_id_for_load(
444 &self,
445 load: &RestoreApplyJournalOperation,
446 ) -> Option<&str> {
447 self.operation_receipts
448 .iter()
449 .find(|receipt| {
450 receipt.matches_load_operation(load)
451 && self.operations.iter().any(|operation| {
452 operation.sequence == receipt.sequence
453 && operation.operation == RestoreApplyOperationKind::UploadSnapshot
454 && operation.state == RestoreApplyOperationState::Completed
455 })
456 })
457 .and_then(|receipt| receipt.uploaded_snapshot_id.as_deref())
458 }
459}