Skip to main content

canic_backup/restore/apply/journal/
mod.rs

1//! Module: restore::apply::journal
2//!
3//! Responsibility: persist and advance restore apply operation state.
4//! Does not own: restore plan construction, command execution, or backup artifact checksums.
5//! Boundary: turns dry-runs into durable journals, reports, previews, and receipts.
6
7mod 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///
44/// RestoreApplyJournal
45///
46/// Durable restore apply operation journal.
47/// Owned by restore apply journaling and consumed by restore runners and reports.
48///
49
50#[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(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    pub operation_receipts: Vec<RestoreApplyOperationReceipt>,
68}
69
70impl RestoreApplyJournal {
71    /// Build the initial no-mutation restore apply journal from a dry-run.
72    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    /// Validate the structural consistency of a restore apply journal.
125    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    /// Build an operator-oriented report from this apply journal.
183    #[must_use]
184    pub(in crate::restore) fn report(&self) -> RestoreApplyJournalReport {
185        RestoreApplyJournalReport::from_journal(self)
186    }
187
188    /// Return the next ready or pending operation that controls runner progress.
189    #[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    /// Render the next transitionable operation as a no-execute command preview.
207    #[must_use]
208    pub fn next_command_preview(&self) -> RestoreApplyCommandPreview {
209        RestoreApplyCommandPreview::from_journal(self)
210    }
211
212    /// Render the next transitionable operation with a configured command preview.
213    #[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    /// Store one durable operation receipt/output and revalidate the journal.
222    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    /// Mark the next transitionable operation pending with an update marker.
236    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    /// Mark one restore apply operation pending with an update marker.
247    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    /// Mark the current pending operation ready again with an update marker.
261    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    /// Mark one restore apply operation ready again with an update marker.
276    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    /// Retry one failed restore apply operation by moving it back to ready.
290    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    /// Mark one restore apply operation completed with an update marker.
304    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    /// Mark one restore apply operation failed with an update marker.
318    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    // Apply one legal operation state transition and revalidate the journal.
337    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    // Ensure fresh operation transitions advance in journal order.
370    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    // Return the next operation sequence that can be advanced by a runner.
394    fn next_transition_sequence(&self) -> Option<usize> {
395        self.next_transition_operation()
396            .map(|operation| operation.sequence)
397    }
398
399    // Recompute operation counts after a journal operation state change.
400    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    // Return whether every planned operation has completed.
412    pub(super) const fn is_complete(&self) -> bool {
413        self.operation_count > 0 && self.completed_operations == self.operation_count
414    }
415
416    // Recompute operation-kind counts from concrete operation rows.
417    pub(super) fn operation_kind_counts(&self) -> RestoreApplyOperationKindCounts {
418        RestoreApplyOperationKindCounts::from_operations(&self.operations)
419    }
420
421    // Ensure one operation attempt has exactly one durable command outcome.
422    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    // Find the uploaded target snapshot ID required by one load operation.
443    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}