Skip to main content

canic_backup/restore/apply/journal/reports/
mod.rs

1use super::{
2    RestoreApplyJournal, RestoreApplyJournalOperation, RestoreApplyOperationKind,
3    RestoreApplyOperationKindCounts, RestoreApplyOperationState,
4};
5use serde::{Deserialize, Serialize};
6
7///
8/// RestoreApplyJournalReport
9///
10
11#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
12pub(in crate::restore) struct RestoreApplyJournalReport {
13    pub report_version: u16,
14    pub backup_id: String,
15    pub outcome: RestoreApplyReportOutcome,
16    pub attention_required: bool,
17    pub ready: bool,
18    pub complete: bool,
19    pub blocked_reasons: Vec<String>,
20    pub operation_count: usize,
21    pub operation_counts: RestoreApplyOperationKindCounts,
22    pub progress: RestoreApplyProgressSummary,
23    pub pending_summary: RestoreApplyPendingSummary,
24    pub pending_operations: usize,
25    pub ready_operations: usize,
26    pub blocked_operations: usize,
27    pub completed_operations: usize,
28    pub failed_operations: usize,
29    pub next_transition: Option<RestoreApplyReportOperation>,
30    pub pending: Vec<RestoreApplyReportOperation>,
31    pub failed: Vec<RestoreApplyReportOperation>,
32    pub blocked: Vec<RestoreApplyReportOperation>,
33}
34
35impl RestoreApplyJournalReport {
36    /// Build a compact operator report from a restore apply journal.
37    #[must_use]
38    pub(in crate::restore) fn from_journal(journal: &RestoreApplyJournal) -> Self {
39        let complete = journal.is_complete();
40        let outcome = RestoreApplyReportOutcome::from_journal(journal, complete);
41        let pending = report_operations_with_state(journal, RestoreApplyOperationState::Pending);
42        let failed = report_operations_with_state(journal, RestoreApplyOperationState::Failed);
43        let blocked = report_operations_with_state(journal, RestoreApplyOperationState::Blocked);
44
45        Self {
46            report_version: 1,
47            backup_id: journal.backup_id.clone(),
48            outcome: outcome.clone(),
49            attention_required: outcome.attention_required(),
50            ready: journal.ready,
51            complete,
52            blocked_reasons: journal.blocked_reasons.clone(),
53            operation_count: journal.operation_count,
54            operation_counts: journal.operation_kind_counts(),
55            progress: RestoreApplyProgressSummary::from_journal(journal),
56            pending_summary: RestoreApplyPendingSummary::from_journal(journal),
57            pending_operations: journal.pending_operations,
58            ready_operations: journal.ready_operations,
59            blocked_operations: journal.blocked_operations,
60            completed_operations: journal.completed_operations,
61            failed_operations: journal.failed_operations,
62            next_transition: journal
63                .next_transition_operation()
64                .map(RestoreApplyReportOperation::from_journal_operation),
65            pending,
66            failed,
67            blocked,
68        }
69    }
70}
71
72///
73/// RestoreApplyPendingSummary
74///
75
76#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
77pub struct RestoreApplyPendingSummary {
78    pub pending_operations: usize,
79    pub pending_operation_available: bool,
80    pub pending_sequence: Option<usize>,
81    pub pending_operation: Option<RestoreApplyOperationKind>,
82    pub pending_updated_at: Option<String>,
83    pub pending_updated_at_known: bool,
84}
85
86impl RestoreApplyPendingSummary {
87    /// Build a compact pending-operation summary from a restore apply journal.
88    #[must_use]
89    pub fn from_journal(journal: &RestoreApplyJournal) -> Self {
90        let pending = journal
91            .operations
92            .iter()
93            .filter(|operation| operation.state == RestoreApplyOperationState::Pending)
94            .min_by_key(|operation| operation.sequence);
95        let pending_updated_at = pending.and_then(|operation| operation.state_updated_at.clone());
96        let pending_updated_at_known = pending_updated_at
97            .as_deref()
98            .is_some_and(known_state_update_marker);
99
100        Self {
101            pending_operations: journal.pending_operations,
102            pending_operation_available: pending.is_some(),
103            pending_sequence: pending.map(|operation| operation.sequence),
104            pending_operation: pending.map(|operation| operation.operation.clone()),
105            pending_updated_at,
106            pending_updated_at_known,
107        }
108    }
109}
110
111// Return whether a journal update marker can be compared by automation.
112fn known_state_update_marker(value: &str) -> bool {
113    !value.trim().is_empty() && value != "unknown"
114}
115
116///
117/// RestoreApplyProgressSummary
118///
119
120#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
121pub struct RestoreApplyProgressSummary {
122    pub operation_count: usize,
123    pub completed_operations: usize,
124    pub remaining_operations: usize,
125    pub transitionable_operations: usize,
126    pub attention_operations: usize,
127    pub completion_basis_points: usize,
128}
129
130impl RestoreApplyProgressSummary {
131    /// Build a compact progress summary from restore apply journal counters.
132    #[must_use]
133    pub const fn from_journal(journal: &RestoreApplyJournal) -> Self {
134        let remaining_operations = journal
135            .operation_count
136            .saturating_sub(journal.completed_operations);
137        let transitionable_operations = journal.ready_operations + journal.pending_operations;
138        let attention_operations =
139            journal.pending_operations + journal.blocked_operations + journal.failed_operations;
140        let completion_basis_points =
141            completion_basis_points(journal.completed_operations, journal.operation_count);
142
143        Self {
144            operation_count: journal.operation_count,
145            completed_operations: journal.completed_operations,
146            remaining_operations,
147            transitionable_operations,
148            attention_operations,
149            completion_basis_points,
150        }
151    }
152}
153
154// Return completion as basis points so JSON stays deterministic and integer-only.
155const fn completion_basis_points(completed_operations: usize, operation_count: usize) -> usize {
156    if operation_count == 0 {
157        return 0;
158    }
159
160    completed_operations.saturating_mul(10_000) / operation_count
161}
162
163///
164/// RestoreApplyReportOutcome
165///
166
167#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
168#[serde(rename_all = "kebab-case")]
169pub enum RestoreApplyReportOutcome {
170    Empty,
171    Complete,
172    Failed,
173    Blocked,
174    Pending,
175    InProgress,
176}
177
178impl RestoreApplyReportOutcome {
179    // Classify the journal into one high-level operator outcome.
180    const fn from_journal(journal: &RestoreApplyJournal, complete: bool) -> Self {
181        if journal.operation_count == 0 {
182            return Self::Empty;
183        }
184        if complete {
185            return Self::Complete;
186        }
187        if journal.failed_operations > 0 {
188            return Self::Failed;
189        }
190        if !journal.ready || journal.blocked_operations > 0 {
191            return Self::Blocked;
192        }
193        if journal.pending_operations > 0 {
194            return Self::Pending;
195        }
196        Self::InProgress
197    }
198
199    // Return whether this outcome needs operator or automation attention.
200    const fn attention_required(&self) -> bool {
201        matches!(self, Self::Failed | Self::Blocked | Self::Pending)
202    }
203}
204
205///
206/// RestoreApplyReportOperation
207///
208
209#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
210pub struct RestoreApplyReportOperation {
211    pub sequence: usize,
212    pub operation: RestoreApplyOperationKind,
213    pub state: RestoreApplyOperationState,
214    pub member_order: usize,
215    pub role: String,
216    pub source_canister: String,
217    pub target_canister: String,
218    pub state_updated_at: Option<String>,
219    pub reasons: Vec<String>,
220}
221
222impl RestoreApplyReportOperation {
223    // Build one compact report row from one journal operation.
224    fn from_journal_operation(operation: &RestoreApplyJournalOperation) -> Self {
225        Self {
226            sequence: operation.sequence,
227            operation: operation.operation.clone(),
228            state: operation.state.clone(),
229            member_order: operation.member_order,
230            role: operation.role.clone(),
231            source_canister: operation.source_canister.clone(),
232            target_canister: operation.target_canister.clone(),
233            state_updated_at: operation.state_updated_at.clone(),
234            reasons: operation.blocking_reasons.clone(),
235        }
236    }
237}
238
239// Return compact report rows for operations in one state.
240fn report_operations_with_state(
241    journal: &RestoreApplyJournal,
242    state: RestoreApplyOperationState,
243) -> Vec<RestoreApplyReportOperation> {
244    journal
245        .operations
246        .iter()
247        .filter(|operation| operation.state == state)
248        .map(RestoreApplyReportOperation::from_journal_operation)
249        .collect()
250}