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(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    /// 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    /// Retry one failed restore apply operation by moving it back to ready.
261    pub fn retry_failed_operation_at(
262        &mut self,
263        sequence: usize,
264        updated_at: Option<String>,
265    ) -> Result<(), RestoreApplyJournalError> {
266        self.transition_operation(
267            sequence,
268            RestoreApplyOperationState::Ready,
269            Vec::new(),
270            updated_at,
271        )
272    }
273
274    /// Mark one restore apply operation completed with an update marker.
275    pub(in crate::restore) fn mark_operation_completed_at(
276        &mut self,
277        sequence: usize,
278        updated_at: Option<String>,
279    ) -> Result<(), RestoreApplyJournalError> {
280        self.transition_operation(
281            sequence,
282            RestoreApplyOperationState::Completed,
283            Vec::new(),
284            updated_at,
285        )
286    }
287
288    /// Mark one restore apply operation failed with an update marker.
289    pub(in crate::restore) fn mark_operation_failed_at(
290        &mut self,
291        sequence: usize,
292        reason: String,
293        updated_at: Option<String>,
294    ) -> Result<(), RestoreApplyJournalError> {
295        if reason.trim().is_empty() {
296            return Err(RestoreApplyJournalError::FailureReasonRequired(sequence));
297        }
298
299        self.transition_operation(
300            sequence,
301            RestoreApplyOperationState::Failed,
302            vec![reason],
303            updated_at,
304        )
305    }
306
307    // Apply one legal operation state transition and revalidate the journal.
308    fn transition_operation(
309        &mut self,
310        sequence: usize,
311        next_state: RestoreApplyOperationState,
312        blocking_reasons: Vec<String>,
313        updated_at: Option<String>,
314    ) -> Result<(), RestoreApplyJournalError> {
315        let index = self
316            .operations
317            .iter()
318            .position(|operation| operation.sequence == sequence)
319            .ok_or(RestoreApplyJournalError::OperationNotFound(sequence))?;
320        let operation = &self.operations[index];
321
322        if !operation.can_transition_to(&next_state) {
323            return Err(RestoreApplyJournalError::InvalidOperationTransition {
324                sequence,
325                from: operation.state.clone(),
326                to: next_state,
327            });
328        }
329
330        self.validate_operation_transition_order(operation, &next_state)?;
331
332        let operation = &mut self.operations[index];
333        operation.state = next_state;
334        operation.blocking_reasons = blocking_reasons;
335        operation.state_updated_at = updated_at;
336        self.refresh_operation_counts();
337        self.validate()
338    }
339
340    // Ensure fresh operation transitions advance in journal order.
341    fn validate_operation_transition_order(
342        &self,
343        operation: &RestoreApplyJournalOperation,
344        next_state: &RestoreApplyOperationState,
345    ) -> Result<(), RestoreApplyJournalError> {
346        if operation.state == *next_state {
347            return Ok(());
348        }
349
350        let next_sequence = self
351            .next_transition_sequence()
352            .ok_or(RestoreApplyJournalError::NoTransitionableOperation)?;
353
354        if operation.sequence == next_sequence {
355            return Ok(());
356        }
357
358        Err(RestoreApplyJournalError::OutOfOrderOperationTransition {
359            requested: operation.sequence,
360            next: next_sequence,
361        })
362    }
363
364    // Return the next operation sequence that can be advanced by a runner.
365    fn next_transition_sequence(&self) -> Option<usize> {
366        self.next_transition_operation()
367            .map(|operation| operation.sequence)
368    }
369
370    // Recompute operation counts after a journal operation state change.
371    fn refresh_operation_counts(&mut self) {
372        let state_counts = RestoreApplyJournalStateCounts::from_operations(&self.operations);
373        self.operation_count = self.operations.len();
374        self.operation_counts = RestoreApplyOperationKindCounts::from_operations(&self.operations);
375        self.pending_operations = state_counts.pending;
376        self.ready_operations = state_counts.ready;
377        self.blocked_operations = state_counts.blocked;
378        self.completed_operations = state_counts.completed;
379        self.failed_operations = state_counts.failed;
380    }
381
382    // Return whether every planned operation has completed.
383    pub(super) const fn is_complete(&self) -> bool {
384        self.operation_count > 0 && self.completed_operations == self.operation_count
385    }
386
387    // Recompute operation-kind counts from concrete operation rows.
388    pub(super) fn operation_kind_counts(&self) -> RestoreApplyOperationKindCounts {
389        RestoreApplyOperationKindCounts::from_operations(&self.operations)
390    }
391
392    // Ensure one operation attempt has exactly one durable command outcome.
393    fn validate_operation_receipt_attempts(&self) -> Result<(), RestoreApplyJournalError> {
394        let mut attempts = BTreeSet::new();
395        for receipt in &self.operation_receipts {
396            if receipt.attempt == 0 {
397                return Err(RestoreApplyJournalError::InvalidOperationReceiptAttempt {
398                    sequence: receipt.sequence,
399                    attempt: receipt.attempt,
400                });
401            }
402            if !attempts.insert((receipt.sequence, receipt.attempt)) {
403                return Err(RestoreApplyJournalError::DuplicateOperationReceiptAttempt {
404                    sequence: receipt.sequence,
405                    attempt: receipt.attempt,
406                });
407            }
408        }
409
410        Ok(())
411    }
412
413    // Find the uploaded target snapshot ID required by one load operation.
414    pub(super) fn uploaded_snapshot_id_for_load(
415        &self,
416        load: &RestoreApplyJournalOperation,
417    ) -> Option<&str> {
418        self.operation_receipts
419            .iter()
420            .find(|receipt| {
421                receipt.matches_load_operation(load)
422                    && self.operations.iter().any(|operation| {
423                        operation.sequence == receipt.sequence
424                            && operation.operation == RestoreApplyOperationKind::UploadSnapshot
425                            && operation.state == RestoreApplyOperationState::Completed
426                    })
427            })
428            .and_then(|receipt| receipt.uploaded_snapshot_id.as_deref())
429    }
430}