1use crate::{
2 execution::{
3 BackupExecutionJournalOperation, BackupExecutionOperationState,
4 BackupExecutionResumeSummary,
5 },
6 journal::ArtifactState,
7 persistence::CommandLifetimeHandle,
8 plan::{BackupExecutionPreflightReceipts, BackupPlan},
9};
10use serde::Serialize;
11use std::{path::Path, path::PathBuf};
12use thiserror::Error as ThisError;
13
14use crate::persistence::JournalLockError;
15
16#[derive(Clone, Debug, Eq, PartialEq)]
21pub struct BackupRunnerConfig {
22 pub out: PathBuf,
23 pub max_steps: Option<usize>,
24 pub updated_at: Option<String>,
25 pub tool_name: String,
26 pub tool_version: String,
27}
28
29pub trait BackupRunnerExecutor {
34 fn preflight_receipts(
36 &mut self,
37 plan: &BackupPlan,
38 preflight_id: &str,
39 validated_at: &str,
40 expires_at: &str,
41 ) -> Result<BackupExecutionPreflightReceipts, BackupRunnerCommandError>;
42
43 fn canister_status(
45 &mut self,
46 canister_id: &str,
47 ) -> Result<BackupRunnerCanisterStatus, BackupRunnerCommandError>;
48
49 fn snapshot_inventory(
51 &mut self,
52 canister_id: &str,
53 ) -> Result<Vec<BackupRunnerSnapshot>, BackupRunnerCommandError>;
54
55 fn stop_canister(
57 &mut self,
58 canister_id: &str,
59 command_lifetime: CommandLifetimeHandle,
60 ) -> Result<(), BackupRunnerCommandError>;
61
62 fn start_canister(
64 &mut self,
65 canister_id: &str,
66 command_lifetime: CommandLifetimeHandle,
67 ) -> Result<(), BackupRunnerCommandError>;
68
69 fn create_snapshot(
71 &mut self,
72 canister_id: &str,
73 command_lifetime: CommandLifetimeHandle,
74 ) -> Result<BackupRunnerSnapshot, BackupRunnerCommandError>;
75
76 fn download_snapshot(
78 &mut self,
79 canister_id: &str,
80 snapshot_id: &str,
81 artifact_path: &Path,
82 command_lifetime: CommandLifetimeHandle,
83 ) -> Result<(), BackupRunnerCommandError>;
84}
85
86#[derive(Clone, Copy, Debug, Eq, PartialEq)]
93pub enum BackupRunnerCanisterStatus {
94 Running,
95 Stopped,
96 Stopping,
97}
98
99impl BackupRunnerCanisterStatus {
100 #[must_use]
101 pub const fn label(self) -> &'static str {
102 match self {
103 Self::Running => "Running",
104 Self::Stopped => "Stopped",
105 Self::Stopping => "Stopping",
106 }
107 }
108}
109
110#[derive(Clone, Debug, Eq, PartialEq)]
115pub struct BackupRunnerSnapshot {
116 pub snapshot_id: String,
117 pub taken_at_timestamp: Option<u64>,
118 pub total_size_bytes: Option<u64>,
119}
120
121#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
126#[error("{status}: {message}")]
127pub struct BackupRunnerCommandError {
128 pub status: String,
129 pub message: String,
130}
131
132impl BackupRunnerCommandError {
133 #[must_use]
134 pub fn failed(status: impl Into<String>, message: impl Into<String>) -> Self {
135 Self {
136 status: status.into(),
137 message: message.into(),
138 }
139 }
140}
141
142#[derive(Debug, ThisError)]
147pub enum BackupRunnerError {
148 #[error("backup execution journal is locked: {lock_path}")]
149 JournalLocked { lock_path: String },
150
151 #[error("backup execution journal lock path is unsafe: {lock_path} ({kind})")]
152 JournalLockUnsafeEntry { lock_path: String, kind: String },
153
154 #[error(
155 "backup operation {sequence} {operation_id} has an external command still running: {lock_path}"
156 )]
157 CommandInFlight {
158 sequence: usize,
159 operation_id: String,
160 lock_path: String,
161 },
162
163 #[error(
164 "backup operation {sequence} {operation_id} has a quiescent command with an unknown external outcome: {lock_path}"
165 )]
166 CommandOutcomeUnknown {
167 sequence: usize,
168 operation_id: String,
169 lock_path: String,
170 },
171
172 #[error(
173 "backup operation {sequence} {operation_id} command lock path is unsafe: {lock_path} ({kind})"
174 )]
175 CommandLockUnsafeEntry {
176 sequence: usize,
177 operation_id: String,
178 lock_path: String,
179 kind: String,
180 },
181
182 #[error(
183 "backup operation {sequence} {operation_id} observed unsettled canister status {status}"
184 )]
185 CanisterStatusUnsettled {
186 sequence: usize,
187 operation_id: String,
188 status: &'static str,
189 },
190
191 #[error("backup operation {sequence} canister status observation failed: {status}: {message}")]
192 CanisterStatusFailed {
193 sequence: usize,
194 status: String,
195 message: String,
196 },
197
198 #[error(
199 "backup operation {sequence} snapshot inventory observation failed: {status}: {message}"
200 )]
201 SnapshotInventoryFailed {
202 sequence: usize,
203 status: String,
204 message: String,
205 },
206
207 #[error(
208 "backup operation {sequence} {operation_id} snapshot inventory lost baseline identities: {snapshot_ids:?}"
209 )]
210 SnapshotInventoryLostBaseline {
211 sequence: usize,
212 operation_id: String,
213 snapshot_ids: Vec<String>,
214 },
215
216 #[error(
217 "backup operation {sequence} {operation_id} snapshot identity is ambiguous: {snapshot_ids:?}"
218 )]
219 SnapshotIdentityAmbiguous {
220 sequence: usize,
221 operation_id: String,
222 snapshot_ids: Vec<String>,
223 },
224
225 #[error(
226 "backup operation {sequence} {operation_id} snapshot inventory contains a blank identity"
227 )]
228 InvalidSnapshotIdentity {
229 sequence: usize,
230 operation_id: String,
231 },
232
233 #[error(
234 "backup operation {sequence} {operation_id} snapshot inventory contains duplicate identity {snapshot_id}"
235 )]
236 DuplicateSnapshotIdentity {
237 sequence: usize,
238 operation_id: String,
239 snapshot_id: String,
240 },
241
242 #[error("backup operation {sequence} {operation_id} is missing its command lifetime handle")]
243 MissingCommandLifetime {
244 sequence: usize,
245 operation_id: String,
246 },
247
248 #[error(
249 "download journal backup id does not match the backup plan: expected={expected}, actual={actual}"
250 )]
251 DownloadJournalBackupIdMismatch { expected: String, actual: String },
252
253 #[error(
254 "download journal topology receipt {field} does not match the backup plan: expected={expected}, actual={actual}"
255 )]
256 DownloadJournalTopologyMismatch {
257 field: &'static str,
258 expected: String,
259 actual: String,
260 },
261
262 #[error("backup operation {sequence} has no target canister")]
263 MissingOperationTarget { sequence: usize },
264
265 #[error("backup operation {sequence} has no snapshot id for target {target_canister_id}")]
266 MissingSnapshotId {
267 sequence: usize,
268 target_canister_id: String,
269 },
270
271 #[error(
272 "backup operation {sequence} has no artifact journal entry for target {target_canister_id}"
273 )]
274 MissingArtifactEntry {
275 sequence: usize,
276 target_canister_id: String,
277 },
278
279 #[error(
280 "backup operation {sequence} has multiple artifact snapshots for target {target_canister_id}: {snapshot_ids:?}"
281 )]
282 AmbiguousArtifactSnapshot {
283 sequence: usize,
284 target_canister_id: String,
285 snapshot_ids: Vec<String>,
286 },
287
288 #[error(
289 "backup operation {sequence} artifact snapshot for target {target_canister_id} does not match the execution receipt: expected={expected_snapshot_id}, actual={actual_snapshot_id}"
290 )]
291 ArtifactDownloadSnapshotMismatch {
292 sequence: usize,
293 target_canister_id: String,
294 expected_snapshot_id: String,
295 actual_snapshot_id: String,
296 },
297
298 #[error(
299 "backup operation {sequence} artifact path for target {target_canister_id} does not match the runner path: journal={journal_path}, expected={expected_path}"
300 )]
301 ArtifactPathMismatch {
302 sequence: usize,
303 target_canister_id: String,
304 journal_path: String,
305 expected_path: String,
306 },
307
308 #[error(
309 "backup operation {sequence} cannot reconcile pending download for target {target_canister_id} while artifact state is {state:?}"
310 )]
311 ArtifactDownloadStateConflict {
312 sequence: usize,
313 target_canister_id: String,
314 state: ArtifactState,
315 },
316
317 #[error(
318 "backup operation {sequence} cannot reconcile pending artifact verification for target {target_canister_id} while artifact state is {state:?}"
319 )]
320 ArtifactVerificationStateConflict {
321 sequence: usize,
322 target_canister_id: String,
323 state: ArtifactState,
324 },
325
326 #[error(
327 "backup manifest exists before finalization operation {sequence} reached a recoverable state: {state:?}"
328 )]
329 PrematureManifest {
330 sequence: usize,
331 state: BackupExecutionOperationState,
332 },
333
334 #[error(
335 "backup operation {sequence} artifact temp path for target {target_canister_id} does not match expected runner path: journal={journal_path}, expected={expected_path}"
336 )]
337 ArtifactTempPathMismatch {
338 sequence: usize,
339 target_canister_id: String,
340 journal_path: String,
341 expected_path: String,
342 },
343
344 #[error(
345 "backup operation {sequence} artifact temp path for target {target_canister_id} is missing: {path}"
346 )]
347 ArtifactTempPathMissing {
348 sequence: usize,
349 target_canister_id: String,
350 path: String,
351 },
352
353 #[error(
354 "backup operation {sequence} artifact temp path for target {target_canister_id} is unsafe: {path} ({kind})"
355 )]
356 ArtifactTempPathUnsafeEntry {
357 sequence: usize,
358 target_canister_id: String,
359 path: String,
360 kind: String,
361 },
362
363 #[error("backup operation {sequence} failed: {status}: {message}")]
364 CommandFailed {
365 sequence: usize,
366 status: String,
367 message: String,
368 },
369
370 #[error("backup failure containment failed after {primary}: {containment}")]
371 FailureContainmentFailed {
372 #[source]
373 primary: Box<Self>,
374 containment: Box<Self>,
375 },
376
377 #[error("backup preflight failed: {status}: {message}")]
378 PreflightFailed { status: String, message: String },
379
380 #[error("backup execution has no operation ready to run")]
381 NoReadyOperation,
382
383 #[error("backup execution is blocked: {reasons:?}")]
384 Blocked { reasons: Vec<String> },
385
386 #[error(transparent)]
387 Io(#[from] std::io::Error),
388
389 #[error(transparent)]
390 Json(#[from] serde_json::Error),
391
392 #[error(transparent)]
393 Persistence(#[from] crate::persistence::PersistenceError),
394
395 #[error(transparent)]
396 BackupPlan(#[from] crate::plan::BackupPlanError),
397
398 #[error(transparent)]
399 ExecutionJournal(#[from] crate::execution::BackupExecutionJournalError),
400
401 #[error(transparent)]
402 Journal(#[from] crate::journal::JournalValidationError),
403
404 #[error(transparent)]
405 Checksum(#[from] crate::artifacts::ArtifactChecksumError),
406
407 #[error(transparent)]
408 Manifest(#[from] crate::manifest::ManifestValidationError),
409}
410
411impl From<JournalLockError> for BackupRunnerError {
412 fn from(error: JournalLockError) -> Self {
413 match error {
414 JournalLockError::Locked { lock_path } => Self::JournalLocked { lock_path },
415 JournalLockError::UnsafeEntry { lock_path, kind } => {
416 Self::JournalLockUnsafeEntry { lock_path, kind }
417 }
418 JournalLockError::Io(error) => Self::Io(error),
419 }
420 }
421}
422
423#[derive(Clone, Debug, Serialize)]
428pub struct BackupRunResponse {
429 pub run_id: String,
430 pub plan_id: String,
431 pub backup_id: String,
432 pub complete: bool,
433 pub max_steps_reached: bool,
434 pub executed_operation_count: usize,
435 pub executed_operations: Vec<BackupRunExecutedOperation>,
436 pub execution: BackupExecutionResumeSummary,
437}
438
439#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
444pub struct BackupRunExecutedOperation {
445 pub sequence: usize,
446 pub operation_id: String,
447 pub kind: String,
448 pub target_canister_id: Option<String>,
449 pub outcome: String,
450}
451
452impl BackupRunExecutedOperation {
453 pub(super) fn completed(operation: &BackupExecutionJournalOperation) -> Self {
454 Self::from_operation(operation, "completed")
455 }
456
457 pub(super) fn failed(operation: &BackupExecutionJournalOperation) -> Self {
458 Self::from_operation(operation, "failed")
459 }
460
461 fn from_operation(operation: &BackupExecutionJournalOperation, outcome: &str) -> Self {
462 Self {
463 sequence: operation.sequence,
464 operation_id: operation.operation_id.clone(),
465 kind: format!("{:?}", operation.kind),
466 target_canister_id: operation.target_canister_id.clone(),
467 outcome: outcome.to_string(),
468 }
469 }
470}