1use crate::{
2 execution::{BackupExecutionJournalOperation, BackupExecutionResumeSummary},
3 persistence::CommandLifetimeHandle,
4 plan::{BackupExecutionPreflightReceipts, BackupPlan},
5};
6use serde::Serialize;
7use std::{error::Error as StdError, fmt, path::Path, path::PathBuf};
8use thiserror::Error as ThisError;
9
10use crate::persistence::JournalLockError;
11
12#[derive(Clone, Debug, Eq, PartialEq)]
17pub struct BackupRunnerConfig {
18 pub out: PathBuf,
19 pub max_steps: Option<usize>,
20 pub updated_at: Option<String>,
21 pub tool_name: String,
22 pub tool_version: String,
23}
24
25pub trait BackupRunnerExecutor {
30 fn preflight_receipts(
32 &mut self,
33 plan: &BackupPlan,
34 preflight_id: &str,
35 validated_at: &str,
36 expires_at: &str,
37 ) -> Result<BackupExecutionPreflightReceipts, BackupRunnerCommandError>;
38
39 fn stop_canister(
41 &mut self,
42 canister_id: &str,
43 command_lifetime: CommandLifetimeHandle,
44 ) -> Result<(), BackupRunnerCommandError>;
45
46 fn start_canister(
48 &mut self,
49 canister_id: &str,
50 command_lifetime: CommandLifetimeHandle,
51 ) -> Result<(), BackupRunnerCommandError>;
52
53 fn create_snapshot(
55 &mut self,
56 canister_id: &str,
57 command_lifetime: CommandLifetimeHandle,
58 ) -> Result<BackupRunnerSnapshotReceipt, BackupRunnerCommandError>;
59
60 fn download_snapshot(
62 &mut self,
63 canister_id: &str,
64 snapshot_id: &str,
65 artifact_path: &Path,
66 command_lifetime: CommandLifetimeHandle,
67 ) -> Result<(), BackupRunnerCommandError>;
68}
69
70#[derive(Clone, Debug, Eq, PartialEq)]
75pub struct BackupRunnerSnapshotReceipt {
76 pub snapshot_id: String,
77 pub taken_at_timestamp: Option<u64>,
78 pub total_size_bytes: Option<u64>,
79}
80
81#[derive(Clone, Debug, Eq, PartialEq)]
86pub struct BackupRunnerCommandError {
87 pub status: String,
88 pub message: String,
89}
90
91impl BackupRunnerCommandError {
92 #[must_use]
93 pub fn failed(status: impl Into<String>, message: impl Into<String>) -> Self {
94 Self {
95 status: status.into(),
96 message: message.into(),
97 }
98 }
99}
100
101impl fmt::Display for BackupRunnerCommandError {
102 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
103 write!(formatter, "{}: {}", self.status, self.message)
104 }
105}
106
107impl StdError for BackupRunnerCommandError {}
108
109#[derive(Debug, ThisError)]
114pub enum BackupRunnerError {
115 #[error("backup execution journal is locked: {lock_path}")]
116 JournalLocked { lock_path: String },
117
118 #[error("backup execution journal lock path is unsafe: {lock_path} ({kind})")]
119 JournalLockUnsafeEntry { lock_path: String, kind: String },
120
121 #[error(
122 "backup operation {sequence} {operation_id} has an external command still running: {lock_path}"
123 )]
124 CommandInFlight {
125 sequence: usize,
126 operation_id: String,
127 lock_path: String,
128 },
129
130 #[error(
131 "backup operation {sequence} {operation_id} has a quiescent command with an unknown external outcome: {lock_path}"
132 )]
133 CommandOutcomeUnknown {
134 sequence: usize,
135 operation_id: String,
136 lock_path: String,
137 },
138
139 #[error(
140 "backup operation {sequence} {operation_id} command lock path is unsafe: {lock_path} ({kind})"
141 )]
142 CommandLockUnsafeEntry {
143 sequence: usize,
144 operation_id: String,
145 lock_path: String,
146 kind: String,
147 },
148
149 #[error("backup operation {sequence} {operation_id} is missing its command lifetime handle")]
150 MissingCommandLifetime {
151 sequence: usize,
152 operation_id: String,
153 },
154
155 #[error(
156 "download journal backup id does not match the backup plan: expected={expected}, actual={actual}"
157 )]
158 DownloadJournalBackupIdMismatch { expected: String, actual: String },
159
160 #[error(
161 "download journal topology receipt {field} does not match the backup plan: expected={expected}, actual={actual}"
162 )]
163 DownloadJournalTopologyMismatch {
164 field: &'static str,
165 expected: String,
166 actual: String,
167 },
168
169 #[error("backup operation {sequence} has no target canister")]
170 MissingOperationTarget { sequence: usize },
171
172 #[error("backup operation {sequence} has no snapshot id for target {target_canister_id}")]
173 MissingSnapshotId {
174 sequence: usize,
175 target_canister_id: String,
176 },
177
178 #[error(
179 "backup operation {sequence} has no artifact journal entry for target {target_canister_id}"
180 )]
181 MissingArtifactEntry {
182 sequence: usize,
183 target_canister_id: String,
184 },
185
186 #[error(
187 "backup operation {sequence} artifact temp path for target {target_canister_id} does not match expected runner path: journal={journal_path}, expected={expected_path}"
188 )]
189 ArtifactTempPathMismatch {
190 sequence: usize,
191 target_canister_id: String,
192 journal_path: String,
193 expected_path: String,
194 },
195
196 #[error("backup operation {sequence} failed: {status}: {message}")]
197 CommandFailed {
198 sequence: usize,
199 status: String,
200 message: String,
201 },
202
203 #[error("backup preflight failed: {status}: {message}")]
204 PreflightFailed { status: String, message: String },
205
206 #[error("backup execution has no operation ready to run")]
207 NoReadyOperation,
208
209 #[error("backup execution is blocked: {reasons:?}")]
210 Blocked { reasons: Vec<String> },
211
212 #[error(transparent)]
213 Io(#[from] std::io::Error),
214
215 #[error(transparent)]
216 Json(#[from] serde_json::Error),
217
218 #[error(transparent)]
219 Persistence(#[from] crate::persistence::PersistenceError),
220
221 #[error(transparent)]
222 BackupPlan(#[from] crate::plan::BackupPlanError),
223
224 #[error(transparent)]
225 ExecutionJournal(#[from] crate::execution::BackupExecutionJournalError),
226
227 #[error(transparent)]
228 Journal(#[from] crate::journal::JournalValidationError),
229
230 #[error(transparent)]
231 Checksum(#[from] crate::artifacts::ArtifactChecksumError),
232
233 #[error(transparent)]
234 Manifest(#[from] crate::manifest::ManifestValidationError),
235}
236
237impl From<JournalLockError> for BackupRunnerError {
238 fn from(error: JournalLockError) -> Self {
239 match error {
240 JournalLockError::Locked { lock_path } => Self::JournalLocked { lock_path },
241 JournalLockError::UnsafeEntry { lock_path, kind } => {
242 Self::JournalLockUnsafeEntry { lock_path, kind }
243 }
244 JournalLockError::Io(error) => Self::Io(error),
245 }
246 }
247}
248
249#[derive(Clone, Debug, Serialize)]
254pub struct BackupRunResponse {
255 pub run_id: String,
256 pub plan_id: String,
257 pub backup_id: String,
258 pub complete: bool,
259 pub max_steps_reached: bool,
260 pub executed_operation_count: usize,
261 pub executed_operations: Vec<BackupRunExecutedOperation>,
262 pub execution: BackupExecutionResumeSummary,
263}
264
265#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
270pub struct BackupRunExecutedOperation {
271 pub sequence: usize,
272 pub operation_id: String,
273 pub kind: String,
274 pub target_canister_id: Option<String>,
275 pub outcome: String,
276}
277
278impl BackupRunExecutedOperation {
279 pub(super) fn completed(operation: &BackupExecutionJournalOperation) -> Self {
280 Self::from_operation(operation, "completed")
281 }
282
283 pub(super) fn failed(operation: &BackupExecutionJournalOperation) -> Self {
284 Self::from_operation(operation, "failed")
285 }
286
287 fn from_operation(operation: &BackupExecutionJournalOperation, outcome: &str) -> Self {
288 Self {
289 sequence: operation.sequence,
290 operation_id: operation.operation_id.clone(),
291 kind: format!("{:?}", operation.kind),
292 target_canister_id: operation.target_canister_id.clone(),
293 outcome: outcome.to_string(),
294 }
295 }
296}