Skip to main content

canic_backup/runner/
mod.rs

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
17/// Execute a persisted backup plan through an injected host executor.
18pub 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;