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#[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
25pub trait BackupRunnerExecutor {
30 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 fn canister_status(
41 &mut self,
42 canister_id: &str,
43 ) -> Result<BackupRunnerCanisterStatus, BackupRunnerCommandError>;
44
45 fn stop_canister(
47 &mut self,
48 canister_id: &str,
49 command_lifetime: CommandLifetimeHandle,
50 ) -> Result<(), BackupRunnerCommandError>;
51
52 fn start_canister(
54 &mut self,
55 canister_id: &str,
56 command_lifetime: CommandLifetimeHandle,
57 ) -> Result<(), BackupRunnerCommandError>;
58
59 fn create_snapshot(
61 &mut self,
62 canister_id: &str,
63 command_lifetime: CommandLifetimeHandle,
64 ) -> Result<BackupRunnerSnapshotReceipt, BackupRunnerCommandError>;
65
66 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#[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#[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#[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#[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#[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#[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}