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::{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, ThisError)]
126#[error("{status}: {message}")]
127pub struct BackupRunnerCommandError {
128    pub status: String,
129    pub message: String,
130}
131
132impl BackupRunnerCommandError {
133    #[must_use]
134    pub fn failed(status: impl Into<String>, message: impl Into<String>) -> Self {
135        Self {
136            status: status.into(),
137            message: message.into(),
138        }
139    }
140}
141
142///
143/// BackupRunnerError
144///
145
146#[derive(Debug, ThisError)]
147pub enum BackupRunnerError {
148    #[error("backup execution journal is locked: {lock_path}")]
149    JournalLocked { lock_path: String },
150
151    #[error("backup execution journal lock path is unsafe: {lock_path} ({kind})")]
152    JournalLockUnsafeEntry { lock_path: String, kind: String },
153
154    #[error(
155        "backup operation {sequence} {operation_id} has an external command still running: {lock_path}"
156    )]
157    CommandInFlight {
158        sequence: usize,
159        operation_id: String,
160        lock_path: String,
161    },
162
163    #[error(
164        "backup operation {sequence} {operation_id} has a quiescent command with an unknown external outcome: {lock_path}"
165    )]
166    CommandOutcomeUnknown {
167        sequence: usize,
168        operation_id: String,
169        lock_path: String,
170    },
171
172    #[error(
173        "backup operation {sequence} {operation_id} command lock path is unsafe: {lock_path} ({kind})"
174    )]
175    CommandLockUnsafeEntry {
176        sequence: usize,
177        operation_id: String,
178        lock_path: String,
179        kind: String,
180    },
181
182    #[error(
183        "backup operation {sequence} {operation_id} observed unsettled canister status {status}"
184    )]
185    CanisterStatusUnsettled {
186        sequence: usize,
187        operation_id: String,
188        status: &'static str,
189    },
190
191    #[error("backup operation {sequence} canister status observation failed: {status}: {message}")]
192    CanisterStatusFailed {
193        sequence: usize,
194        status: String,
195        message: String,
196    },
197
198    #[error(
199        "backup operation {sequence} snapshot inventory observation failed: {status}: {message}"
200    )]
201    SnapshotInventoryFailed {
202        sequence: usize,
203        status: String,
204        message: String,
205    },
206
207    #[error(
208        "backup operation {sequence} {operation_id} snapshot inventory lost baseline identities: {snapshot_ids:?}"
209    )]
210    SnapshotInventoryLostBaseline {
211        sequence: usize,
212        operation_id: String,
213        snapshot_ids: Vec<String>,
214    },
215
216    #[error(
217        "backup operation {sequence} {operation_id} snapshot identity is ambiguous: {snapshot_ids:?}"
218    )]
219    SnapshotIdentityAmbiguous {
220        sequence: usize,
221        operation_id: String,
222        snapshot_ids: Vec<String>,
223    },
224
225    #[error(
226        "backup operation {sequence} {operation_id} snapshot inventory contains a blank identity"
227    )]
228    InvalidSnapshotIdentity {
229        sequence: usize,
230        operation_id: String,
231    },
232
233    #[error(
234        "backup operation {sequence} {operation_id} snapshot inventory contains duplicate identity {snapshot_id}"
235    )]
236    DuplicateSnapshotIdentity {
237        sequence: usize,
238        operation_id: String,
239        snapshot_id: String,
240    },
241
242    #[error("backup operation {sequence} {operation_id} is missing its command lifetime handle")]
243    MissingCommandLifetime {
244        sequence: usize,
245        operation_id: String,
246    },
247
248    #[error(
249        "download journal backup id does not match the backup plan: expected={expected}, actual={actual}"
250    )]
251    DownloadJournalBackupIdMismatch { expected: String, actual: String },
252
253    #[error(
254        "download journal topology receipt {field} does not match the backup plan: expected={expected}, actual={actual}"
255    )]
256    DownloadJournalTopologyMismatch {
257        field: &'static str,
258        expected: String,
259        actual: String,
260    },
261
262    #[error("backup operation {sequence} has no target canister")]
263    MissingOperationTarget { sequence: usize },
264
265    #[error("backup operation {sequence} has no snapshot id for target {target_canister_id}")]
266    MissingSnapshotId {
267        sequence: usize,
268        target_canister_id: String,
269    },
270
271    #[error(
272        "backup operation {sequence} has no artifact journal entry for target {target_canister_id}"
273    )]
274    MissingArtifactEntry {
275        sequence: usize,
276        target_canister_id: String,
277    },
278
279    #[error(
280        "backup operation {sequence} has multiple artifact snapshots for target {target_canister_id}: {snapshot_ids:?}"
281    )]
282    AmbiguousArtifactSnapshot {
283        sequence: usize,
284        target_canister_id: String,
285        snapshot_ids: Vec<String>,
286    },
287
288    #[error(
289        "backup operation {sequence} artifact snapshot for target {target_canister_id} does not match the execution receipt: expected={expected_snapshot_id}, actual={actual_snapshot_id}"
290    )]
291    ArtifactDownloadSnapshotMismatch {
292        sequence: usize,
293        target_canister_id: String,
294        expected_snapshot_id: String,
295        actual_snapshot_id: String,
296    },
297
298    #[error(
299        "backup operation {sequence} artifact path for target {target_canister_id} does not match the runner path: journal={journal_path}, expected={expected_path}"
300    )]
301    ArtifactPathMismatch {
302        sequence: usize,
303        target_canister_id: String,
304        journal_path: String,
305        expected_path: String,
306    },
307
308    #[error(
309        "backup operation {sequence} cannot reconcile pending download for target {target_canister_id} while artifact state is {state:?}"
310    )]
311    ArtifactDownloadStateConflict {
312        sequence: usize,
313        target_canister_id: String,
314        state: ArtifactState,
315    },
316
317    #[error(
318        "backup operation {sequence} cannot reconcile pending artifact verification for target {target_canister_id} while artifact state is {state:?}"
319    )]
320    ArtifactVerificationStateConflict {
321        sequence: usize,
322        target_canister_id: String,
323        state: ArtifactState,
324    },
325
326    #[error(
327        "backup manifest exists before finalization operation {sequence} reached a recoverable state: {state:?}"
328    )]
329    PrematureManifest {
330        sequence: usize,
331        state: BackupExecutionOperationState,
332    },
333
334    #[error(
335        "backup operation {sequence} artifact temp path for target {target_canister_id} does not match expected runner path: journal={journal_path}, expected={expected_path}"
336    )]
337    ArtifactTempPathMismatch {
338        sequence: usize,
339        target_canister_id: String,
340        journal_path: String,
341        expected_path: String,
342    },
343
344    #[error(
345        "backup operation {sequence} artifact temp path for target {target_canister_id} is missing: {path}"
346    )]
347    ArtifactTempPathMissing {
348        sequence: usize,
349        target_canister_id: String,
350        path: String,
351    },
352
353    #[error(
354        "backup operation {sequence} artifact temp path for target {target_canister_id} is unsafe: {path} ({kind})"
355    )]
356    ArtifactTempPathUnsafeEntry {
357        sequence: usize,
358        target_canister_id: String,
359        path: String,
360        kind: String,
361    },
362
363    #[error("backup operation {sequence} failed: {status}: {message}")]
364    CommandFailed {
365        sequence: usize,
366        status: String,
367        message: String,
368    },
369
370    #[error("backup failure containment failed after {primary}: {containment}")]
371    FailureContainmentFailed {
372        #[source]
373        primary: Box<Self>,
374        containment: Box<Self>,
375    },
376
377    #[error("backup preflight failed: {status}: {message}")]
378    PreflightFailed { status: String, message: String },
379
380    #[error("backup execution has no operation ready to run")]
381    NoReadyOperation,
382
383    #[error("backup execution is blocked: {reasons:?}")]
384    Blocked { reasons: Vec<String> },
385
386    #[error(transparent)]
387    Io(#[from] std::io::Error),
388
389    #[error(transparent)]
390    Json(#[from] serde_json::Error),
391
392    #[error(transparent)]
393    Persistence(#[from] crate::persistence::PersistenceError),
394
395    #[error(transparent)]
396    BackupPlan(#[from] crate::plan::BackupPlanError),
397
398    #[error(transparent)]
399    ExecutionJournal(#[from] crate::execution::BackupExecutionJournalError),
400
401    #[error(transparent)]
402    Journal(#[from] crate::journal::JournalValidationError),
403
404    #[error(transparent)]
405    Checksum(#[from] crate::artifacts::ArtifactChecksumError),
406
407    #[error(transparent)]
408    Manifest(#[from] crate::manifest::ManifestValidationError),
409}
410
411impl From<JournalLockError> for BackupRunnerError {
412    fn from(error: JournalLockError) -> Self {
413        match error {
414            JournalLockError::Locked { lock_path } => Self::JournalLocked { lock_path },
415            JournalLockError::UnsafeEntry { lock_path, kind } => {
416                Self::JournalLockUnsafeEntry { lock_path, kind }
417            }
418            JournalLockError::Io(error) => Self::Io(error),
419        }
420    }
421}
422
423///
424/// BackupRunResponse
425///
426
427#[derive(Clone, Debug, Serialize)]
428pub struct BackupRunResponse {
429    pub run_id: String,
430    pub plan_id: String,
431    pub backup_id: String,
432    pub complete: bool,
433    pub max_steps_reached: bool,
434    pub executed_operation_count: usize,
435    pub executed_operations: Vec<BackupRunExecutedOperation>,
436    pub execution: BackupExecutionResumeSummary,
437}
438
439///
440/// BackupRunExecutedOperation
441///
442
443#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
444pub struct BackupRunExecutedOperation {
445    pub sequence: usize,
446    pub operation_id: String,
447    pub kind: String,
448    pub target_canister_id: Option<String>,
449    pub outcome: String,
450}
451
452impl BackupRunExecutedOperation {
453    pub(super) fn completed(operation: &BackupExecutionJournalOperation) -> Self {
454        Self::from_operation(operation, "completed")
455    }
456
457    pub(super) fn failed(operation: &BackupExecutionJournalOperation) -> Self {
458        Self::from_operation(operation, "failed")
459    }
460
461    fn from_operation(operation: &BackupExecutionJournalOperation, outcome: &str) -> Self {
462        Self {
463            sequence: operation.sequence,
464            operation_id: operation.operation_id.clone(),
465            kind: format!("{:?}", operation.kind),
466            target_canister_id: operation.target_canister_id.clone(),
467            outcome: outcome.to_string(),
468        }
469    }
470}