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