Skip to main content

canic_backup/runner/
types.rs

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