canic_backup/restore/apply/journal/reports/
mod.rs1use super::{
2 RestoreApplyJournal, RestoreApplyJournalOperation, RestoreApplyOperationKind,
3 RestoreApplyOperationKindCounts, RestoreApplyOperationState,
4};
5use serde::{Deserialize, Serialize};
6
7#[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 #[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#[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 #[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
111fn known_state_update_marker(value: &str) -> bool {
113 !value.trim().is_empty() && value != "unknown"
114}
115
116#[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 #[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
154const 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#[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 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 const fn attention_required(&self) -> bool {
201 matches!(self, Self::Failed | Self::Blocked | Self::Pending)
202 }
203}
204
205#[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 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
239fn 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}