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 snapshot_inventory(
47 &mut self,
48 canister_id: &str,
49 ) -> Result<Vec<BackupRunnerSnapshot>, BackupRunnerCommandError>;
50
51 fn stop_canister(
53 &mut self,
54 canister_id: &str,
55 command_lifetime: CommandLifetimeHandle,
56 ) -> Result<(), BackupRunnerCommandError>;
57
58 fn start_canister(
60 &mut self,
61 canister_id: &str,
62 command_lifetime: CommandLifetimeHandle,
63 ) -> Result<(), BackupRunnerCommandError>;
64
65 fn create_snapshot(
67 &mut self,
68 canister_id: &str,
69 command_lifetime: CommandLifetimeHandle,
70 ) -> Result<BackupRunnerSnapshot, BackupRunnerCommandError>;
71
72 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#[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#[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#[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#[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#[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#[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}