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