canic_backup/restore/apply/journal/reports/
mod.rs1use super::{
8 RestoreApplyJournal, RestoreApplyJournalOperation, RestoreApplyOperationKind,
9 RestoreApplyOperationKindCounts, RestoreApplyOperationState,
10};
11
12use serde::{Deserialize, Serialize};
13
14#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
22#[serde(deny_unknown_fields)]
23pub(in crate::restore) struct RestoreApplyJournalReport {
24 pub report_version: u16,
25 pub backup_id: String,
26 pub outcome: RestoreApplyReportOutcome,
27 pub attention_required: bool,
28 pub ready: bool,
29 pub complete: bool,
30 pub blocked_reasons: Vec<String>,
31 pub operation_count: usize,
32 pub operation_counts: RestoreApplyOperationKindCounts,
33 pub progress: RestoreApplyProgressSummary,
34 pub pending_summary: RestoreApplyPendingSummary,
35 pub pending_operations: usize,
36 pub ready_operations: usize,
37 pub blocked_operations: usize,
38 pub completed_operations: usize,
39 pub failed_operations: usize,
40 pub next_transition: Option<RestoreApplyReportOperation>,
41 pub pending: Vec<RestoreApplyReportOperation>,
42 pub failed: Vec<RestoreApplyReportOperation>,
43 pub blocked: Vec<RestoreApplyReportOperation>,
44}
45
46impl RestoreApplyJournalReport {
47 #[must_use]
49 pub(in crate::restore) fn from_journal(journal: &RestoreApplyJournal) -> Self {
50 let complete = journal.is_complete();
51 let outcome = RestoreApplyReportOutcome::from_journal(journal, complete);
52 let pending = report_operations_with_state(journal, RestoreApplyOperationState::Pending);
53 let failed = report_operations_with_state(journal, RestoreApplyOperationState::Failed);
54 let blocked = report_operations_with_state(journal, RestoreApplyOperationState::Blocked);
55
56 Self {
57 report_version: 1,
58 backup_id: journal.backup_id.clone(),
59 outcome: outcome.clone(),
60 attention_required: outcome.attention_required(),
61 ready: journal.ready,
62 complete,
63 blocked_reasons: journal.blocked_reasons.clone(),
64 operation_count: journal.operation_count,
65 operation_counts: journal.operation_kind_counts(),
66 progress: RestoreApplyProgressSummary::from_journal(journal),
67 pending_summary: RestoreApplyPendingSummary::from_journal(journal),
68 pending_operations: journal.pending_operations,
69 ready_operations: journal.ready_operations,
70 blocked_operations: journal.blocked_operations,
71 completed_operations: journal.completed_operations,
72 failed_operations: journal.failed_operations,
73 next_transition: journal
74 .next_transition_operation()
75 .map(RestoreApplyReportOperation::from_journal_operation),
76 pending,
77 failed,
78 blocked,
79 }
80 }
81}
82
83#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
91#[serde(deny_unknown_fields)]
92pub struct RestoreApplyPendingSummary {
93 pub pending_operations: usize,
94 pub pending_operation_available: bool,
95 pub pending_sequence: Option<usize>,
96 pub pending_operation: Option<RestoreApplyOperationKind>,
97 pub pending_updated_at: Option<String>,
98 pub pending_updated_at_known: bool,
99}
100
101impl RestoreApplyPendingSummary {
102 #[must_use]
104 pub fn from_journal(journal: &RestoreApplyJournal) -> Self {
105 let pending = journal
106 .operations
107 .iter()
108 .filter(|operation| operation.state == RestoreApplyOperationState::Pending)
109 .min_by_key(|operation| operation.sequence);
110 let pending_updated_at = pending.and_then(|operation| operation.state_updated_at.clone());
111 let pending_updated_at_known = pending_updated_at
112 .as_deref()
113 .is_some_and(known_state_update_marker);
114
115 Self {
116 pending_operations: journal.pending_operations,
117 pending_operation_available: pending.is_some(),
118 pending_sequence: pending.map(|operation| operation.sequence),
119 pending_operation: pending.map(|operation| operation.operation.clone()),
120 pending_updated_at,
121 pending_updated_at_known,
122 }
123 }
124}
125
126fn known_state_update_marker(value: &str) -> bool {
127 !value.trim().is_empty() && value != "unknown"
128}
129
130#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
138#[serde(deny_unknown_fields)]
139pub struct RestoreApplyProgressSummary {
140 pub operation_count: usize,
141 pub completed_operations: usize,
142 pub remaining_operations: usize,
143 pub transitionable_operations: usize,
144 pub attention_operations: usize,
145 pub completion_basis_points: usize,
146}
147
148impl RestoreApplyProgressSummary {
149 #[must_use]
151 pub const fn from_journal(journal: &RestoreApplyJournal) -> Self {
152 let remaining_operations = journal
153 .operation_count
154 .saturating_sub(journal.completed_operations);
155 let transitionable_operations = journal.ready_operations + journal.pending_operations;
156 let attention_operations =
157 journal.pending_operations + journal.blocked_operations + journal.failed_operations;
158 let completion_basis_points =
159 completion_basis_points(journal.completed_operations, journal.operation_count);
160
161 Self {
162 operation_count: journal.operation_count,
163 completed_operations: journal.completed_operations,
164 remaining_operations,
165 transitionable_operations,
166 attention_operations,
167 completion_basis_points,
168 }
169 }
170}
171
172const fn completion_basis_points(completed_operations: usize, operation_count: usize) -> usize {
173 if operation_count == 0 {
174 return 0;
175 }
176
177 completed_operations.saturating_mul(10_000) / operation_count
178}
179
180#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
188#[serde(rename_all = "kebab-case")]
189pub enum RestoreApplyReportOutcome {
190 Empty,
191 Complete,
192 Failed,
193 Blocked,
194 Pending,
195 InProgress,
196}
197
198impl RestoreApplyReportOutcome {
199 const fn from_journal(journal: &RestoreApplyJournal, complete: bool) -> Self {
200 if journal.operation_count == 0 {
201 return Self::Empty;
202 }
203 if complete {
204 return Self::Complete;
205 }
206 if journal.failed_operations > 0 {
207 return Self::Failed;
208 }
209 if !journal.ready || journal.blocked_operations > 0 {
210 return Self::Blocked;
211 }
212 if journal.pending_operations > 0 {
213 return Self::Pending;
214 }
215 Self::InProgress
216 }
217
218 const fn attention_required(&self) -> bool {
219 matches!(self, Self::Failed | Self::Blocked | Self::Pending)
220 }
221}
222
223#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
231#[serde(deny_unknown_fields)]
232pub struct RestoreApplyReportOperation {
233 pub sequence: usize,
234 pub operation: RestoreApplyOperationKind,
235 pub state: RestoreApplyOperationState,
236 pub member_order: usize,
237 pub role: String,
238 pub source_canister: String,
239 pub target_canister: String,
240 pub state_updated_at: Option<String>,
241 pub reasons: Vec<String>,
242}
243
244impl RestoreApplyReportOperation {
245 fn from_journal_operation(operation: &RestoreApplyJournalOperation) -> Self {
246 Self {
247 sequence: operation.sequence,
248 operation: operation.operation.clone(),
249 state: operation.state.clone(),
250 member_order: operation.member_order,
251 role: operation.role.clone(),
252 source_canister: operation.source_canister.clone(),
253 target_canister: operation.target_canister.clone(),
254 state_updated_at: operation.state_updated_at.clone(),
255 reasons: operation.blocking_reasons.clone(),
256 }
257 }
258}
259
260fn report_operations_with_state(
261 journal: &RestoreApplyJournal,
262 state: RestoreApplyOperationState,
263) -> Vec<RestoreApplyReportOperation> {
264 journal
265 .operations
266 .iter()
267 .filter(|operation| operation.state == state)
268 .map(RestoreApplyReportOperation::from_journal_operation)
269 .collect()
270}