1mod manifest;
2mod operations;
3mod types;
4
5pub use types::*;
6
7use crate::{
8 execution::{BackupExecutionJournal, BackupExecutionOperationState},
9 persistence::{BackupLayout, CommandLifetimeLock, CommandLifetimeLockError, JournalLock},
10 plan::BackupPlan,
11 timestamp::{state_updated_at, timestamp_marker, timestamp_seconds},
12};
13use operations::execute_operation_receipt;
14
15const PREFLIGHT_TTL_SECONDS: u64 = 300;
16
17pub fn backup_run_execute_with_executor(
19 config: &BackupRunnerConfig,
20 executor: &mut impl BackupRunnerExecutor,
21) -> Result<BackupRunResponse, BackupRunnerError> {
22 let layout = BackupLayout::new(config.out.clone());
23 let _lock = JournalLock::acquire(&layout.execution_journal_path())?;
24 let mut plan = layout.read_backup_plan()?;
25 let mut journal = if layout.execution_journal_path().is_file() {
26 layout.read_execution_journal()?
27 } else {
28 let journal = BackupExecutionJournal::from_plan(&plan)?;
29 layout.write_execution_journal(&journal)?;
30 journal
31 };
32 layout.verify_execution_integrity()?;
33
34 accept_preflight_if_needed(config, executor, &layout, &mut plan, &mut journal)?;
35 execute_ready_operations(config, executor, &layout, &plan, &mut journal)
36}
37
38fn accept_preflight_if_needed(
39 config: &BackupRunnerConfig,
40 executor: &mut impl BackupRunnerExecutor,
41 layout: &BackupLayout,
42 plan: &mut BackupPlan,
43 journal: &mut BackupExecutionJournal,
44) -> Result<(), BackupRunnerError> {
45 if journal.preflight_accepted {
46 return Ok(());
47 }
48
49 let validated_at = state_updated_at(config.updated_at.as_ref());
50 let expires_at = timestamp_marker(timestamp_seconds(&validated_at) + PREFLIGHT_TTL_SECONDS);
51 let preflight_id = format!("preflight-{}", plan.run_id);
52 let receipts = executor
53 .preflight_receipts(plan, &preflight_id, &validated_at, &expires_at)
54 .map_err(|error| BackupRunnerError::PreflightFailed {
55 status: error.status,
56 message: error.message,
57 })?;
58 plan.apply_execution_preflight_receipts(&receipts, &validated_at)?;
59 layout.write_backup_plan(plan)?;
60 journal.accept_preflight_receipts_at(&receipts, Some(validated_at))?;
61 layout.write_execution_journal(journal)?;
62 Ok(())
63}
64
65fn execute_ready_operations(
66 config: &BackupRunnerConfig,
67 executor: &mut impl BackupRunnerExecutor,
68 layout: &BackupLayout,
69 plan: &BackupPlan,
70 journal: &mut BackupExecutionJournal,
71) -> Result<BackupRunResponse, BackupRunnerError> {
72 let mut executed = Vec::new();
73
74 loop {
75 let summary = journal.resume_summary();
76 if summary.completed_operations + summary.skipped_operations == summary.total_operations {
77 return Ok(run_response(plan, journal, executed, false));
78 }
79 if config
80 .max_steps
81 .is_some_and(|max_steps| executed.len() >= max_steps)
82 {
83 return Ok(run_response(plan, journal, executed, true));
84 }
85
86 let operation = journal
87 .next_ready_operation()
88 .cloned()
89 .ok_or(BackupRunnerError::NoReadyOperation)?;
90 if operation.state == BackupExecutionOperationState::Blocked {
91 return Err(BackupRunnerError::Blocked {
92 reasons: operation.blocking_reasons,
93 });
94 }
95
96 let mut command_lock = backup_command_lock(layout, &operation)?;
97 if operation.state == BackupExecutionOperationState::Pending {
98 reject_unknown_backup_command_outcome(&operation, command_lock.take())?;
99 }
100
101 journal.mark_operation_pending_at(
102 operation.sequence,
103 Some(state_updated_at(config.updated_at.as_ref())),
104 )?;
105 layout.write_execution_journal(journal)?;
106
107 let operation_result = execute_operation_receipt(
108 config,
109 executor,
110 layout,
111 plan,
112 journal,
113 &operation,
114 command_lock.as_ref().map(CommandLifetimeLock::handle),
115 );
116 if let Some(command_lock) = command_lock {
117 command_lock
118 .finish()
119 .map_err(|error| backup_command_lock_error(&operation, error))?;
120 }
121
122 match operation_result {
123 Ok(receipt) => {
124 journal.record_operation_receipt(receipt)?;
125 layout.write_execution_journal(journal)?;
126 executed.push(BackupRunExecutedOperation::completed(&operation));
127 }
128 Err(error) => {
129 let receipt = crate::execution::BackupExecutionOperationReceipt::failed(
130 journal,
131 &operation,
132 Some(state_updated_at(config.updated_at.as_ref())),
133 error.to_string(),
134 );
135 journal.record_operation_receipt(receipt)?;
136 layout.write_execution_journal(journal)?;
137 executed.push(BackupRunExecutedOperation::failed(&operation));
138 return Err(error);
139 }
140 }
141 }
142}
143
144fn reject_unknown_backup_command_outcome(
145 operation: &crate::execution::BackupExecutionJournalOperation,
146 command_lock: Option<CommandLifetimeLock>,
147) -> Result<(), BackupRunnerError> {
148 let Some(command_lock) = command_lock else {
149 return Ok(());
150 };
151 let lock_path = command_lock.path().to_string_lossy().to_string();
152 command_lock
153 .finish()
154 .map_err(|error| backup_command_lock_error(operation, error))?;
155 Err(BackupRunnerError::CommandOutcomeUnknown {
156 sequence: operation.sequence,
157 operation_id: operation.operation_id.clone(),
158 lock_path,
159 })
160}
161
162fn backup_command_lock(
163 layout: &BackupLayout,
164 operation: &crate::execution::BackupExecutionJournalOperation,
165) -> Result<Option<CommandLifetimeLock>, BackupRunnerError> {
166 if !matches!(
167 operation.kind,
168 crate::plan::BackupOperationKind::Stop
169 | crate::plan::BackupOperationKind::CreateSnapshot
170 | crate::plan::BackupOperationKind::Start
171 | crate::plan::BackupOperationKind::DownloadSnapshot
172 ) {
173 return Ok(None);
174 }
175
176 CommandLifetimeLock::acquire(&layout.execution_journal_path(), operation.sequence)
177 .map(Some)
178 .map_err(|error| backup_command_lock_error(operation, error))
179}
180
181fn backup_command_lock_error(
182 operation: &crate::execution::BackupExecutionJournalOperation,
183 error: CommandLifetimeLockError,
184) -> BackupRunnerError {
185 match error {
186 CommandLifetimeLockError::InFlight { lock_path } => BackupRunnerError::CommandInFlight {
187 sequence: operation.sequence,
188 operation_id: operation.operation_id.clone(),
189 lock_path,
190 },
191 CommandLifetimeLockError::UnsafeEntry { lock_path, kind } => {
192 BackupRunnerError::CommandLockUnsafeEntry {
193 sequence: operation.sequence,
194 operation_id: operation.operation_id.clone(),
195 lock_path,
196 kind,
197 }
198 }
199 CommandLifetimeLockError::Io(error) => BackupRunnerError::Io(error),
200 }
201}
202
203fn run_response(
204 plan: &BackupPlan,
205 journal: &BackupExecutionJournal,
206 executed: Vec<BackupRunExecutedOperation>,
207 max_steps_reached: bool,
208) -> BackupRunResponse {
209 let execution = journal.resume_summary();
210 BackupRunResponse {
211 run_id: plan.run_id.clone(),
212 plan_id: plan.plan_id.clone(),
213 backup_id: plan.run_id.clone(),
214 complete: execution.completed_operations + execution.skipped_operations
215 == execution.total_operations,
216 max_steps_reached,
217 executed_operation_count: executed.len(),
218 executed_operations: executed,
219 execution,
220 }
221}
222
223#[cfg(test)]
224mod tests;