1use crate::{
2 execution::{BackupExecutionJournalOperation, BackupExecutionResumeSummary},
3 journal::ArtifactState,
4 persistence::CommandLifetimeHandle,
5 plan::{BackupExecutionPreflightReceipts, BackupPlan},
6};
7use serde::Serialize;
8use std::{error::Error as StdError, fmt, path::Path, path::PathBuf};
9use thiserror::Error as ThisError;
10
11use crate::persistence::JournalLockError;
12
13#[derive(Clone, Debug, Eq, PartialEq)]
18pub struct BackupRunnerConfig {
19 pub out: PathBuf,
20 pub max_steps: Option<usize>,
21 pub updated_at: Option<String>,
22 pub tool_name: String,
23 pub tool_version: String,
24}
25
26pub trait BackupRunnerExecutor {
31 fn preflight_receipts(
33 &mut self,
34 plan: &BackupPlan,
35 preflight_id: &str,
36 validated_at: &str,
37 expires_at: &str,
38 ) -> Result<BackupExecutionPreflightReceipts, BackupRunnerCommandError>;
39
40 fn canister_status(
42 &mut self,
43 canister_id: &str,
44 ) -> Result<BackupRunnerCanisterStatus, BackupRunnerCommandError>;
45
46 fn snapshot_inventory(
48 &mut self,
49 canister_id: &str,
50 ) -> Result<Vec<BackupRunnerSnapshot>, BackupRunnerCommandError>;
51
52 fn stop_canister(
54 &mut self,
55 canister_id: &str,
56 command_lifetime: CommandLifetimeHandle,
57 ) -> Result<(), BackupRunnerCommandError>;
58
59 fn start_canister(
61 &mut self,
62 canister_id: &str,
63 command_lifetime: CommandLifetimeHandle,
64 ) -> Result<(), BackupRunnerCommandError>;
65
66 fn create_snapshot(
68 &mut self,
69 canister_id: &str,
70 command_lifetime: CommandLifetimeHandle,
71 ) -> Result<BackupRunnerSnapshot, BackupRunnerCommandError>;
72
73 fn download_snapshot(
75 &mut self,
76 canister_id: &str,
77 snapshot_id: &str,
78 artifact_path: &Path,
79 command_lifetime: CommandLifetimeHandle,
80 ) -> Result<(), BackupRunnerCommandError>;
81}
82
83#[derive(Clone, Copy, Debug, Eq, PartialEq)]
90pub enum BackupRunnerCanisterStatus {
91 Running,
92 Stopped,
93 Stopping,
94}
95
96impl BackupRunnerCanisterStatus {
97 #[must_use]
98 pub const fn label(self) -> &'static str {
99 match self {
100 Self::Running => "Running",
101 Self::Stopped => "Stopped",
102 Self::Stopping => "Stopping",
103 }
104 }
105}
106
107#[derive(Clone, Debug, Eq, PartialEq)]
112pub struct BackupRunnerSnapshot {
113 pub snapshot_id: String,
114 pub taken_at_timestamp: Option<u64>,
115 pub total_size_bytes: Option<u64>,
116}
117
118#[derive(Clone, Debug, Eq, PartialEq)]
123pub struct BackupRunnerCommandError {
124 pub status: String,
125 pub message: String,
126}
127
128impl BackupRunnerCommandError {
129 #[must_use]
130 pub fn failed(status: impl Into<String>, message: impl Into<String>) -> Self {
131 Self {
132 status: status.into(),
133 message: message.into(),
134 }
135 }
136}
137
138impl fmt::Display for BackupRunnerCommandError {
139 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
140 write!(formatter, "{}: {}", self.status, self.message)
141 }
142}
143
144impl StdError for BackupRunnerCommandError {}
145
146#[derive(Debug, ThisError)]
151pub enum BackupRunnerError {
152 #[error("backup execution journal is locked: {lock_path}")]
153 JournalLocked { lock_path: String },
154
155 #[error("backup execution journal lock path is unsafe: {lock_path} ({kind})")]
156 JournalLockUnsafeEntry { lock_path: String, kind: String },
157
158 #[error(
159 "backup operation {sequence} {operation_id} has an external command still running: {lock_path}"
160 )]
161 CommandInFlight {
162 sequence: usize,
163 operation_id: String,
164 lock_path: String,
165 },
166
167 #[error(
168 "backup operation {sequence} {operation_id} has a quiescent command with an unknown external outcome: {lock_path}"
169 )]
170 CommandOutcomeUnknown {
171 sequence: usize,
172 operation_id: String,
173 lock_path: String,
174 },
175
176 #[error(
177 "backup operation {sequence} {operation_id} command lock path is unsafe: {lock_path} ({kind})"
178 )]
179 CommandLockUnsafeEntry {
180 sequence: usize,
181 operation_id: String,
182 lock_path: String,
183 kind: String,
184 },
185
186 #[error(
187 "backup operation {sequence} {operation_id} observed unsettled canister status {status}"
188 )]
189 CanisterStatusUnsettled {
190 sequence: usize,
191 operation_id: String,
192 status: &'static str,
193 },
194
195 #[error("backup operation {sequence} canister status observation failed: {status}: {message}")]
196 CanisterStatusFailed {
197 sequence: usize,
198 status: String,
199 message: String,
200 },
201
202 #[error(
203 "backup operation {sequence} snapshot inventory observation failed: {status}: {message}"
204 )]
205 SnapshotInventoryFailed {
206 sequence: usize,
207 status: String,
208 message: String,
209 },
210
211 #[error(
212 "backup operation {sequence} {operation_id} snapshot inventory lost baseline identities: {snapshot_ids:?}"
213 )]
214 SnapshotInventoryLostBaseline {
215 sequence: usize,
216 operation_id: String,
217 snapshot_ids: Vec<String>,
218 },
219
220 #[error(
221 "backup operation {sequence} {operation_id} snapshot identity is ambiguous: {snapshot_ids:?}"
222 )]
223 SnapshotIdentityAmbiguous {
224 sequence: usize,
225 operation_id: String,
226 snapshot_ids: Vec<String>,
227 },
228
229 #[error(
230 "backup operation {sequence} {operation_id} snapshot inventory contains a blank identity"
231 )]
232 InvalidSnapshotIdentity {
233 sequence: usize,
234 operation_id: String,
235 },
236
237 #[error(
238 "backup operation {sequence} {operation_id} snapshot inventory contains duplicate identity {snapshot_id}"
239 )]
240 DuplicateSnapshotIdentity {
241 sequence: usize,
242 operation_id: String,
243 snapshot_id: String,
244 },
245
246 #[error("backup operation {sequence} {operation_id} is missing its command lifetime handle")]
247 MissingCommandLifetime {
248 sequence: usize,
249 operation_id: String,
250 },
251
252 #[error(
253 "download journal backup id does not match the backup plan: expected={expected}, actual={actual}"
254 )]
255 DownloadJournalBackupIdMismatch { expected: String, actual: String },
256
257 #[error(
258 "download journal topology receipt {field} does not match the backup plan: expected={expected}, actual={actual}"
259 )]
260 DownloadJournalTopologyMismatch {
261 field: &'static str,
262 expected: String,
263 actual: String,
264 },
265
266 #[error("backup operation {sequence} has no target canister")]
267 MissingOperationTarget { sequence: usize },
268
269 #[error("backup operation {sequence} has no snapshot id for target {target_canister_id}")]
270 MissingSnapshotId {
271 sequence: usize,
272 target_canister_id: String,
273 },
274
275 #[error(
276 "backup operation {sequence} has no artifact journal entry for target {target_canister_id}"
277 )]
278 MissingArtifactEntry {
279 sequence: usize,
280 target_canister_id: String,
281 },
282
283 #[error(
284 "backup operation {sequence} has multiple artifact snapshots for target {target_canister_id}: {snapshot_ids:?}"
285 )]
286 AmbiguousArtifactSnapshot {
287 sequence: usize,
288 target_canister_id: String,
289 snapshot_ids: Vec<String>,
290 },
291
292 #[error(
293 "backup operation {sequence} artifact snapshot for target {target_canister_id} does not match the execution receipt: expected={expected_snapshot_id}, actual={actual_snapshot_id}"
294 )]
295 ArtifactDownloadSnapshotMismatch {
296 sequence: usize,
297 target_canister_id: String,
298 expected_snapshot_id: String,
299 actual_snapshot_id: String,
300 },
301
302 #[error(
303 "backup operation {sequence} artifact path for target {target_canister_id} does not match the runner path: journal={journal_path}, expected={expected_path}"
304 )]
305 ArtifactPathMismatch {
306 sequence: usize,
307 target_canister_id: String,
308 journal_path: String,
309 expected_path: String,
310 },
311
312 #[error(
313 "backup operation {sequence} cannot reconcile pending download for target {target_canister_id} while artifact state is {state:?}"
314 )]
315 ArtifactDownloadStateConflict {
316 sequence: usize,
317 target_canister_id: String,
318 state: ArtifactState,
319 },
320
321 #[error(
322 "backup operation {sequence} artifact temp path for target {target_canister_id} does not match expected runner path: journal={journal_path}, expected={expected_path}"
323 )]
324 ArtifactTempPathMismatch {
325 sequence: usize,
326 target_canister_id: String,
327 journal_path: String,
328 expected_path: String,
329 },
330
331 #[error(
332 "backup operation {sequence} artifact temp path for target {target_canister_id} is missing: {path}"
333 )]
334 ArtifactTempPathMissing {
335 sequence: usize,
336 target_canister_id: String,
337 path: String,
338 },
339
340 #[error(
341 "backup operation {sequence} artifact temp path for target {target_canister_id} is unsafe: {path} ({kind})"
342 )]
343 ArtifactTempPathUnsafeEntry {
344 sequence: usize,
345 target_canister_id: String,
346 path: String,
347 kind: String,
348 },
349
350 #[error("backup operation {sequence} failed: {status}: {message}")]
351 CommandFailed {
352 sequence: usize,
353 status: String,
354 message: String,
355 },
356
357 #[error("backup preflight failed: {status}: {message}")]
358 PreflightFailed { status: String, message: String },
359
360 #[error("backup execution has no operation ready to run")]
361 NoReadyOperation,
362
363 #[error("backup execution is blocked: {reasons:?}")]
364 Blocked { reasons: Vec<String> },
365
366 #[error(transparent)]
367 Io(#[from] std::io::Error),
368
369 #[error(transparent)]
370 Json(#[from] serde_json::Error),
371
372 #[error(transparent)]
373 Persistence(#[from] crate::persistence::PersistenceError),
374
375 #[error(transparent)]
376 BackupPlan(#[from] crate::plan::BackupPlanError),
377
378 #[error(transparent)]
379 ExecutionJournal(#[from] crate::execution::BackupExecutionJournalError),
380
381 #[error(transparent)]
382 Journal(#[from] crate::journal::JournalValidationError),
383
384 #[error(transparent)]
385 Checksum(#[from] crate::artifacts::ArtifactChecksumError),
386
387 #[error(transparent)]
388 Manifest(#[from] crate::manifest::ManifestValidationError),
389}
390
391impl From<JournalLockError> for BackupRunnerError {
392 fn from(error: JournalLockError) -> Self {
393 match error {
394 JournalLockError::Locked { lock_path } => Self::JournalLocked { lock_path },
395 JournalLockError::UnsafeEntry { lock_path, kind } => {
396 Self::JournalLockUnsafeEntry { lock_path, kind }
397 }
398 JournalLockError::Io(error) => Self::Io(error),
399 }
400 }
401}
402
403#[derive(Clone, Debug, Serialize)]
408pub struct BackupRunResponse {
409 pub run_id: String,
410 pub plan_id: String,
411 pub backup_id: String,
412 pub complete: bool,
413 pub max_steps_reached: bool,
414 pub executed_operation_count: usize,
415 pub executed_operations: Vec<BackupRunExecutedOperation>,
416 pub execution: BackupExecutionResumeSummary,
417}
418
419#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
424pub struct BackupRunExecutedOperation {
425 pub sequence: usize,
426 pub operation_id: String,
427 pub kind: String,
428 pub target_canister_id: Option<String>,
429 pub outcome: String,
430}
431
432impl BackupRunExecutedOperation {
433 pub(super) fn completed(operation: &BackupExecutionJournalOperation) -> Self {
434 Self::from_operation(operation, "completed")
435 }
436
437 pub(super) fn failed(operation: &BackupExecutionJournalOperation) -> Self {
438 Self::from_operation(operation, "failed")
439 }
440
441 fn from_operation(operation: &BackupExecutionJournalOperation, outcome: &str) -> Self {
442 Self {
443 sequence: operation.sequence,
444 operation_id: operation.operation_id.clone(),
445 kind: format!("{:?}", operation.kind),
446 target_canister_id: operation.target_canister_id.clone(),
447 outcome: outcome.to_string(),
448 }
449 }
450}