Skip to main content

canic_backup/runner/
types.rs

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///
14/// BackupRunnerConfig
15///
16
17#[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
26///
27/// BackupRunnerExecutor
28///
29
30pub trait BackupRunnerExecutor {
31    /// Prove execution preflights before any mutating operation runs.
32    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    /// Observe one canister's authoritative lifecycle status.
41    fn canister_status(
42        &mut self,
43        canister_id: &str,
44    ) -> Result<BackupRunnerCanisterStatus, BackupRunnerCommandError>;
45
46    /// Observe the authoritative snapshots currently retained for one canister.
47    fn snapshot_inventory(
48        &mut self,
49        canister_id: &str,
50    ) -> Result<Vec<BackupRunnerSnapshot>, BackupRunnerCommandError>;
51
52    /// Stop one selected canister.
53    fn stop_canister(
54        &mut self,
55        canister_id: &str,
56        command_lifetime: CommandLifetimeHandle,
57    ) -> Result<(), BackupRunnerCommandError>;
58
59    /// Start one selected canister.
60    fn start_canister(
61        &mut self,
62        canister_id: &str,
63        command_lifetime: CommandLifetimeHandle,
64    ) -> Result<(), BackupRunnerCommandError>;
65
66    /// Create one selected canister snapshot and return the typed snapshot receipt.
67    fn create_snapshot(
68        &mut self,
69        canister_id: &str,
70        command_lifetime: CommandLifetimeHandle,
71    ) -> Result<BackupRunnerSnapshot, BackupRunnerCommandError>;
72
73    /// Download one selected snapshot into a temporary artifact directory.
74    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///
84/// BackupRunnerCanisterStatus
85///
86/// Typed lifecycle status used to reconcile interrupted backup operations.
87///
88
89#[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///
108/// BackupRunnerSnapshot
109///
110
111#[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///
119/// BackupRunnerCommandError
120///
121
122#[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///
147/// BackupRunnerError
148///
149
150#[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///
404/// BackupRunResponse
405///
406
407#[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///
420/// BackupRunExecutedOperation
421///
422
423#[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}