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