1mod operation;
8mod receipt;
9#[cfg(test)]
10mod tests;
11mod types;
12mod validation;
13
14pub use types::*;
15
16use crate::plan::{BackupExecutionPreflightReceipts, BackupOperationKind, BackupPlan};
17use validation::{
18 operation_kind_is_mutating, operation_kind_is_preflight, validate_nonempty,
19 validate_operation_sequences,
20};
21
22const BACKUP_EXECUTION_JOURNAL_VERSION: u16 = 1;
23const PREFLIGHT_NOT_ACCEPTED: &str = "preflight-not-accepted";
24
25impl BackupExecutionJournal {
26 pub fn from_plan(plan: &BackupPlan) -> Result<Self, BackupExecutionJournalError> {
28 plan.validate()
29 .map_err(|error| BackupExecutionJournalError::InvalidPlan(error.to_string()))?;
30 let operations = plan
31 .phases
32 .iter()
33 .map(BackupExecutionJournalOperation::from_plan_operation)
34 .collect::<Vec<_>>();
35 let mut journal = Self {
36 journal_version: BACKUP_EXECUTION_JOURNAL_VERSION,
37 plan_id: plan.plan_id.clone(),
38 run_id: plan.run_id.clone(),
39 preflight_id: None,
40 preflight_accepted: false,
41 restart_required: false,
42 operations,
43 operation_receipts: Vec::new(),
44 };
45 journal.refresh_blocked_operations();
46 journal.validate()?;
47 Ok(journal)
48 }
49
50 pub fn validate(&self) -> Result<(), BackupExecutionJournalError> {
52 if self.journal_version != BACKUP_EXECUTION_JOURNAL_VERSION {
53 return Err(BackupExecutionJournalError::UnsupportedVersion(
54 self.journal_version,
55 ));
56 }
57 validate_nonempty("plan_id", &self.plan_id)?;
58 validate_nonempty("run_id", &self.run_id)?;
59 if let Some(preflight_id) = &self.preflight_id {
60 validate_nonempty("preflight_id", preflight_id)?;
61 } else if self.preflight_accepted {
62 return Err(BackupExecutionJournalError::AcceptedPreflightMissingId);
63 }
64 if self.restart_required != self.derived_restart_required() {
65 return Err(BackupExecutionJournalError::RestartRequiredMismatch);
66 }
67 validate_operation_sequences(&self.operations)?;
68 for operation in &self.operations {
69 operation.validate()?;
70 if !self.preflight_accepted && operation_kind_is_mutating(&operation.kind) {
71 match operation.state {
72 BackupExecutionOperationState::Blocked => {}
73 BackupExecutionOperationState::Ready
74 | BackupExecutionOperationState::Pending
75 | BackupExecutionOperationState::Completed
76 | BackupExecutionOperationState::Failed
77 | BackupExecutionOperationState::Skipped => {
78 return Err(BackupExecutionJournalError::MutationReadyBeforePreflight {
79 sequence: operation.sequence,
80 });
81 }
82 }
83 }
84 }
85 for receipt in &self.operation_receipts {
86 receipt.validate_against(self)?;
87 }
88 Ok(())
89 }
90
91 pub fn accept_preflight_bundle_at(
93 &mut self,
94 preflight_id: String,
95 updated_at: Option<String>,
96 ) -> Result<(), BackupExecutionJournalError> {
97 validate_nonempty("preflight_id", &preflight_id)?;
98 validate_nonempty("updated_at", updated_at.as_deref().unwrap_or_default())?;
99 if let Some(existing) = &self.preflight_id
100 && existing != &preflight_id
101 {
102 return Err(BackupExecutionJournalError::PreflightAlreadyAccepted {
103 existing: existing.clone(),
104 attempted: preflight_id,
105 });
106 }
107
108 self.preflight_id = Some(preflight_id);
109 self.preflight_accepted = true;
110 for operation in &mut self.operations {
111 if operation_kind_is_preflight(&operation.kind) {
112 operation.state = BackupExecutionOperationState::Completed;
113 operation.state_updated_at.clone_from(&updated_at);
114 operation.blocking_reasons.clear();
115 } else if operation.state == BackupExecutionOperationState::Blocked {
116 operation.state = BackupExecutionOperationState::Ready;
117 operation.blocking_reasons.clear();
118 }
119 }
120 self.refresh_restart_required();
121 self.validate()
122 }
123
124 pub fn accept_preflight_receipts_at(
126 &mut self,
127 receipts: &BackupExecutionPreflightReceipts,
128 updated_at: Option<String>,
129 ) -> Result<(), BackupExecutionJournalError> {
130 validate_nonempty("preflight_receipts.plan_id", &receipts.plan_id)?;
131 if receipts.plan_id != self.plan_id {
132 return Err(BackupExecutionJournalError::PreflightPlanMismatch {
133 expected: self.plan_id.clone(),
134 actual: receipts.plan_id.clone(),
135 });
136 }
137 self.accept_preflight_bundle_at(receipts.preflight_id.clone(), updated_at)
138 }
139
140 #[must_use]
142 pub fn next_ready_operation(&self) -> Option<&BackupExecutionJournalOperation> {
143 self.operations
144 .iter()
145 .filter(|operation| {
146 matches!(
147 operation.state,
148 BackupExecutionOperationState::Ready
149 | BackupExecutionOperationState::Pending
150 | BackupExecutionOperationState::Failed
151 )
152 })
153 .min_by_key(|operation| operation.sequence)
154 }
155
156 pub fn mark_next_operation_pending_at(
158 &mut self,
159 updated_at: Option<String>,
160 ) -> Result<(), BackupExecutionJournalError> {
161 let sequence = self
162 .next_ready_operation()
163 .ok_or(BackupExecutionJournalError::NoTransitionableOperation)?
164 .sequence;
165 self.mark_operation_pending_at(sequence, updated_at)
166 }
167
168 pub fn mark_operation_pending_at(
170 &mut self,
171 sequence: usize,
172 updated_at: Option<String>,
173 ) -> Result<(), BackupExecutionJournalError> {
174 self.mark_operation_pending_with_snapshot_inventory_at(sequence, updated_at, None)
175 }
176
177 pub fn mark_snapshot_create_pending_at(
179 &mut self,
180 sequence: usize,
181 updated_at: Option<String>,
182 snapshot_ids_before: Vec<String>,
183 ) -> Result<(), BackupExecutionJournalError> {
184 self.mark_operation_pending_with_snapshot_inventory_at(
185 sequence,
186 updated_at,
187 Some(snapshot_ids_before),
188 )
189 }
190
191 fn mark_operation_pending_with_snapshot_inventory_at(
192 &mut self,
193 sequence: usize,
194 updated_at: Option<String>,
195 snapshot_ids_before: Option<Vec<String>>,
196 ) -> Result<(), BackupExecutionJournalError> {
197 validate_nonempty("updated_at", updated_at.as_deref().unwrap_or_default())?;
198 let expected = self
199 .next_ready_operation()
200 .ok_or(BackupExecutionJournalError::NoTransitionableOperation)?
201 .sequence;
202 if sequence != expected {
203 return Err(BackupExecutionJournalError::OutOfOrderOperationTransition {
204 requested: sequence,
205 next: expected,
206 });
207 }
208 let index = self.operation_index(sequence)?;
209 let operation = &self.operations[index];
210 if operation_kind_is_mutating(&operation.kind) && !self.preflight_accepted {
211 return Err(BackupExecutionJournalError::MutationBeforePreflightAccepted { sequence });
212 }
213 if !matches!(
214 operation.state,
215 BackupExecutionOperationState::Ready | BackupExecutionOperationState::Failed
216 ) {
217 return Err(BackupExecutionJournalError::InvalidOperationTransition {
218 sequence,
219 from: operation.state.clone(),
220 to: BackupExecutionOperationState::Pending,
221 });
222 }
223
224 let previous_operation = self.operations[index].clone();
225 let previous_restart_required = self.restart_required;
226 let operation = &mut self.operations[index];
227 operation.state = BackupExecutionOperationState::Pending;
228 operation.state_updated_at = updated_at;
229 operation.snapshot_ids_before = snapshot_ids_before;
230 operation.blocking_reasons.clear();
231 self.refresh_restart_required();
232 if let Err(error) = self.validate() {
233 self.operations[index] = previous_operation;
234 self.restart_required = previous_restart_required;
235 return Err(error);
236 }
237 Ok(())
238 }
239
240 pub fn record_operation_receipt(
242 &mut self,
243 receipt: BackupExecutionOperationReceipt,
244 ) -> Result<(), BackupExecutionJournalError> {
245 receipt.validate_against(self)?;
246 let index = self.operation_index(receipt.sequence)?;
247 let operation = &self.operations[index];
248 if operation.state != BackupExecutionOperationState::Pending {
249 return Err(
250 BackupExecutionJournalError::ReceiptWithoutPendingOperation {
251 sequence: receipt.sequence,
252 },
253 );
254 }
255
256 let next_state = match receipt.outcome {
257 BackupExecutionOperationReceiptOutcome::Completed => {
258 BackupExecutionOperationState::Completed
259 }
260 BackupExecutionOperationReceiptOutcome::Failed => BackupExecutionOperationState::Failed,
261 BackupExecutionOperationReceiptOutcome::Skipped => {
262 BackupExecutionOperationState::Skipped
263 }
264 };
265 let failure_reason = receipt.failure_reason.clone();
266 let previous_operation = self.operations[index].clone();
267 let previous_restart_required = self.restart_required;
268 self.operation_receipts.push(receipt);
269
270 let operation = &mut self.operations[index];
271 operation.state = next_state;
272 operation.state_updated_at = self
273 .operation_receipts
274 .last()
275 .and_then(|receipt| receipt.updated_at.clone());
276 operation.blocking_reasons = failure_reason.into_iter().collect();
277 self.refresh_restart_required();
278 if let Err(error) = self.validate() {
279 self.operation_receipts.pop();
280 self.operations[index] = previous_operation;
281 self.restart_required = previous_restart_required;
282 return Err(error);
283 }
284 Ok(())
285 }
286
287 pub fn retry_failed_operation_at(
289 &mut self,
290 sequence: usize,
291 updated_at: Option<String>,
292 ) -> Result<(), BackupExecutionJournalError> {
293 validate_nonempty("updated_at", updated_at.as_deref().unwrap_or_default())?;
294 let index = self.operation_index(sequence)?;
295 if self.operations[index].state != BackupExecutionOperationState::Failed {
296 return Err(BackupExecutionJournalError::OperationNotFailed(sequence));
297 }
298 self.operations[index].state = BackupExecutionOperationState::Ready;
299 self.operations[index].state_updated_at = updated_at;
300 self.operations[index].blocking_reasons.clear();
301 self.refresh_restart_required();
302 self.validate()
303 }
304
305 #[must_use]
307 pub fn resume_summary(&self) -> BackupExecutionResumeSummary {
308 let mut summary = BackupExecutionResumeSummary {
309 plan_id: self.plan_id.clone(),
310 run_id: self.run_id.clone(),
311 preflight_id: self.preflight_id.clone(),
312 preflight_accepted: self.preflight_accepted,
313 restart_required: self.restart_required,
314 total_operations: self.operations.len(),
315 ready_operations: 0,
316 pending_operations: 0,
317 blocked_operations: 0,
318 completed_operations: 0,
319 failed_operations: 0,
320 skipped_operations: 0,
321 next_operation: self.next_ready_operation().cloned(),
322 };
323 for operation in &self.operations {
324 match operation.state {
325 BackupExecutionOperationState::Ready => summary.ready_operations += 1,
326 BackupExecutionOperationState::Pending => summary.pending_operations += 1,
327 BackupExecutionOperationState::Blocked => summary.blocked_operations += 1,
328 BackupExecutionOperationState::Completed => summary.completed_operations += 1,
329 BackupExecutionOperationState::Failed => summary.failed_operations += 1,
330 BackupExecutionOperationState::Skipped => summary.skipped_operations += 1,
331 }
332 }
333 summary
334 }
335
336 fn operation_index(&self, sequence: usize) -> Result<usize, BackupExecutionJournalError> {
337 self.operations
338 .iter()
339 .position(|operation| operation.sequence == sequence)
340 .ok_or(BackupExecutionJournalError::OperationNotFound(sequence))
341 }
342
343 fn refresh_blocked_operations(&mut self) {
344 if self.preflight_accepted {
345 return;
346 }
347 for operation in &mut self.operations {
348 if operation_kind_is_mutating(&operation.kind) {
349 operation.state = BackupExecutionOperationState::Blocked;
350 operation.blocking_reasons = vec![PREFLIGHT_NOT_ACCEPTED.to_string()];
351 }
352 }
353 }
354
355 fn refresh_restart_required(&mut self) {
356 self.restart_required = self.derived_restart_required();
357 }
358
359 fn derived_restart_required(&self) -> bool {
360 let stopped = self.operations.iter().any(|operation| {
361 operation.kind == BackupOperationKind::Stop
362 && operation.state == BackupExecutionOperationState::Completed
363 });
364 let unstarted = self.operations.iter().any(|operation| {
365 operation.kind == BackupOperationKind::Start
366 && !matches!(
367 operation.state,
368 BackupExecutionOperationState::Completed
369 | BackupExecutionOperationState::Skipped
370 )
371 });
372 stopped && unstarted
373 }
374}