Skip to main content

canic_backup/runner/
types.rs

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///
13/// BackupRunnerConfig
14///
15
16#[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
25///
26/// BackupRunnerExecutor
27///
28
29pub trait BackupRunnerExecutor {
30    /// Prove execution preflights before any mutating operation runs.
31    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    /// Observe one canister's authoritative lifecycle status.
40    fn canister_status(
41        &mut self,
42        canister_id: &str,
43    ) -> Result<BackupRunnerCanisterStatus, BackupRunnerCommandError>;
44
45    /// Stop one selected canister.
46    fn stop_canister(
47        &mut self,
48        canister_id: &str,
49        command_lifetime: CommandLifetimeHandle,
50    ) -> Result<(), BackupRunnerCommandError>;
51
52    /// Start one selected canister.
53    fn start_canister(
54        &mut self,
55        canister_id: &str,
56        command_lifetime: CommandLifetimeHandle,
57    ) -> Result<(), BackupRunnerCommandError>;
58
59    /// Create one selected canister snapshot and return the typed snapshot receipt.
60    fn create_snapshot(
61        &mut self,
62        canister_id: &str,
63        command_lifetime: CommandLifetimeHandle,
64    ) -> Result<BackupRunnerSnapshotReceipt, BackupRunnerCommandError>;
65
66    /// Download one selected snapshot into a temporary artifact directory.
67    fn download_snapshot(
68        &mut self,
69        canister_id: &str,
70        snapshot_id: &str,
71        artifact_path: &Path,
72        command_lifetime: CommandLifetimeHandle,
73    ) -> Result<(), BackupRunnerCommandError>;
74}
75
76///
77/// BackupRunnerCanisterStatus
78///
79/// Typed lifecycle status used to reconcile interrupted backup operations.
80///
81
82#[derive(Clone, Copy, Debug, Eq, PartialEq)]
83pub enum BackupRunnerCanisterStatus {
84    Running,
85    Stopped,
86    Stopping,
87}
88
89impl BackupRunnerCanisterStatus {
90    #[must_use]
91    pub const fn label(self) -> &'static str {
92        match self {
93            Self::Running => "Running",
94            Self::Stopped => "Stopped",
95            Self::Stopping => "Stopping",
96        }
97    }
98}
99
100///
101/// BackupRunnerSnapshotReceipt
102///
103
104#[derive(Clone, Debug, Eq, PartialEq)]
105pub struct BackupRunnerSnapshotReceipt {
106    pub snapshot_id: String,
107    pub taken_at_timestamp: Option<u64>,
108    pub total_size_bytes: Option<u64>,
109}
110
111///
112/// BackupRunnerCommandError
113///
114
115#[derive(Clone, Debug, Eq, PartialEq)]
116pub struct BackupRunnerCommandError {
117    pub status: String,
118    pub message: String,
119}
120
121impl BackupRunnerCommandError {
122    #[must_use]
123    pub fn failed(status: impl Into<String>, message: impl Into<String>) -> Self {
124        Self {
125            status: status.into(),
126            message: message.into(),
127        }
128    }
129}
130
131impl fmt::Display for BackupRunnerCommandError {
132    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
133        write!(formatter, "{}: {}", self.status, self.message)
134    }
135}
136
137impl StdError for BackupRunnerCommandError {}
138
139///
140/// BackupRunnerError
141///
142
143#[derive(Debug, ThisError)]
144pub enum BackupRunnerError {
145    #[error("backup execution journal is locked: {lock_path}")]
146    JournalLocked { lock_path: String },
147
148    #[error("backup execution journal lock path is unsafe: {lock_path} ({kind})")]
149    JournalLockUnsafeEntry { lock_path: String, kind: String },
150
151    #[error(
152        "backup operation {sequence} {operation_id} has an external command still running: {lock_path}"
153    )]
154    CommandInFlight {
155        sequence: usize,
156        operation_id: String,
157        lock_path: String,
158    },
159
160    #[error(
161        "backup operation {sequence} {operation_id} has a quiescent command with an unknown external outcome: {lock_path}"
162    )]
163    CommandOutcomeUnknown {
164        sequence: usize,
165        operation_id: String,
166        lock_path: String,
167    },
168
169    #[error(
170        "backup operation {sequence} {operation_id} command lock path is unsafe: {lock_path} ({kind})"
171    )]
172    CommandLockUnsafeEntry {
173        sequence: usize,
174        operation_id: String,
175        lock_path: String,
176        kind: String,
177    },
178
179    #[error(
180        "backup operation {sequence} {operation_id} observed unsettled canister status {status}"
181    )]
182    CanisterStatusUnsettled {
183        sequence: usize,
184        operation_id: String,
185        status: &'static str,
186    },
187
188    #[error("backup operation {sequence} canister status observation failed: {status}: {message}")]
189    CanisterStatusFailed {
190        sequence: usize,
191        status: String,
192        message: String,
193    },
194
195    #[error("backup operation {sequence} {operation_id} is missing its command lifetime handle")]
196    MissingCommandLifetime {
197        sequence: usize,
198        operation_id: String,
199    },
200
201    #[error(
202        "download journal backup id does not match the backup plan: expected={expected}, actual={actual}"
203    )]
204    DownloadJournalBackupIdMismatch { expected: String, actual: String },
205
206    #[error(
207        "download journal topology receipt {field} does not match the backup plan: expected={expected}, actual={actual}"
208    )]
209    DownloadJournalTopologyMismatch {
210        field: &'static str,
211        expected: String,
212        actual: String,
213    },
214
215    #[error("backup operation {sequence} has no target canister")]
216    MissingOperationTarget { sequence: usize },
217
218    #[error("backup operation {sequence} has no snapshot id for target {target_canister_id}")]
219    MissingSnapshotId {
220        sequence: usize,
221        target_canister_id: String,
222    },
223
224    #[error(
225        "backup operation {sequence} has no artifact journal entry for target {target_canister_id}"
226    )]
227    MissingArtifactEntry {
228        sequence: usize,
229        target_canister_id: String,
230    },
231
232    #[error(
233        "backup operation {sequence} artifact temp path for target {target_canister_id} does not match expected runner path: journal={journal_path}, expected={expected_path}"
234    )]
235    ArtifactTempPathMismatch {
236        sequence: usize,
237        target_canister_id: String,
238        journal_path: String,
239        expected_path: String,
240    },
241
242    #[error("backup operation {sequence} failed: {status}: {message}")]
243    CommandFailed {
244        sequence: usize,
245        status: String,
246        message: String,
247    },
248
249    #[error("backup preflight failed: {status}: {message}")]
250    PreflightFailed { status: String, message: String },
251
252    #[error("backup execution has no operation ready to run")]
253    NoReadyOperation,
254
255    #[error("backup execution is blocked: {reasons:?}")]
256    Blocked { reasons: Vec<String> },
257
258    #[error(transparent)]
259    Io(#[from] std::io::Error),
260
261    #[error(transparent)]
262    Json(#[from] serde_json::Error),
263
264    #[error(transparent)]
265    Persistence(#[from] crate::persistence::PersistenceError),
266
267    #[error(transparent)]
268    BackupPlan(#[from] crate::plan::BackupPlanError),
269
270    #[error(transparent)]
271    ExecutionJournal(#[from] crate::execution::BackupExecutionJournalError),
272
273    #[error(transparent)]
274    Journal(#[from] crate::journal::JournalValidationError),
275
276    #[error(transparent)]
277    Checksum(#[from] crate::artifacts::ArtifactChecksumError),
278
279    #[error(transparent)]
280    Manifest(#[from] crate::manifest::ManifestValidationError),
281}
282
283impl From<JournalLockError> for BackupRunnerError {
284    fn from(error: JournalLockError) -> Self {
285        match error {
286            JournalLockError::Locked { lock_path } => Self::JournalLocked { lock_path },
287            JournalLockError::UnsafeEntry { lock_path, kind } => {
288                Self::JournalLockUnsafeEntry { lock_path, kind }
289            }
290            JournalLockError::Io(error) => Self::Io(error),
291        }
292    }
293}
294
295///
296/// BackupRunResponse
297///
298
299#[derive(Clone, Debug, Serialize)]
300pub struct BackupRunResponse {
301    pub run_id: String,
302    pub plan_id: String,
303    pub backup_id: String,
304    pub complete: bool,
305    pub max_steps_reached: bool,
306    pub executed_operation_count: usize,
307    pub executed_operations: Vec<BackupRunExecutedOperation>,
308    pub execution: BackupExecutionResumeSummary,
309}
310
311///
312/// BackupRunExecutedOperation
313///
314
315#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
316pub struct BackupRunExecutedOperation {
317    pub sequence: usize,
318    pub operation_id: String,
319    pub kind: String,
320    pub target_canister_id: Option<String>,
321    pub outcome: String,
322}
323
324impl BackupRunExecutedOperation {
325    pub(super) fn completed(operation: &BackupExecutionJournalOperation) -> Self {
326        Self::from_operation(operation, "completed")
327    }
328
329    pub(super) fn failed(operation: &BackupExecutionJournalOperation) -> Self {
330        Self::from_operation(operation, "failed")
331    }
332
333    fn from_operation(operation: &BackupExecutionJournalOperation, outcome: &str) -> Self {
334        Self {
335            sequence: operation.sequence,
336            operation_id: operation.operation_id.clone(),
337            kind: format!("{:?}", operation.kind),
338            target_canister_id: operation.target_canister_id.clone(),
339            outcome: outcome.to_string(),
340        }
341    }
342}