Skip to main content

canic_backup/execution/
types.rs

1//! Module: execution::types
2//!
3//! Responsibility: define serialized backup execution journal contracts.
4//! Does not own: transition logic, backup planning, or artifact IO.
5//! Boundary: durable journal and receipt shapes shared by backup runners.
6
7use crate::plan::BackupOperationKind;
8
9use serde::{Deserialize, Serialize};
10use thiserror::Error as ThisError;
11
12///
13/// BackupExecutionJournal
14///
15/// Durable execution journal for one backup plan run.
16/// Owned by backup execution and persisted for resume and integrity checks.
17///
18
19#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
20#[serde(deny_unknown_fields)]
21pub struct BackupExecutionJournal {
22    pub journal_version: u16,
23    pub plan_id: String,
24    pub run_id: String,
25    pub preflight_id: Option<String>,
26    pub preflight_accepted: bool,
27    pub restart_required: bool,
28    pub operations: Vec<BackupExecutionJournalOperation>,
29    pub operation_receipts: Vec<BackupExecutionOperationReceipt>,
30}
31
32///
33/// BackupExecutionJournalOperation
34///
35/// One ordered operation tracked by the backup execution journal.
36/// Owned by backup execution and derived from validated backup plans.
37///
38
39#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
40#[serde(deny_unknown_fields)]
41pub struct BackupExecutionJournalOperation {
42    pub sequence: usize,
43    pub operation_id: String,
44    pub kind: BackupOperationKind,
45    pub target_canister_id: Option<String>,
46    pub state: BackupExecutionOperationState,
47    pub state_updated_at: Option<String>,
48    pub blocking_reasons: Vec<String>,
49}
50
51///
52/// BackupExecutionOperationState
53///
54/// Durable runner state for one backup execution operation.
55/// Owned by backup execution and interpreted by resume logic.
56///
57
58#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
59#[serde(rename_all = "kebab-case")]
60pub enum BackupExecutionOperationState {
61    Ready,
62    Pending,
63    Blocked,
64    Completed,
65    Failed,
66    Skipped,
67}
68
69///
70/// BackupExecutionOperationReceipt
71///
72/// Durable receipt for one attempted backup operation transition.
73/// Owned by backup execution and checked against journal state.
74///
75
76#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
77#[serde(deny_unknown_fields)]
78pub struct BackupExecutionOperationReceipt {
79    pub plan_id: String,
80    pub run_id: String,
81    pub preflight_id: Option<String>,
82    pub sequence: usize,
83    pub operation_id: String,
84    pub kind: BackupOperationKind,
85    pub target_canister_id: Option<String>,
86    pub outcome: BackupExecutionOperationReceiptOutcome,
87    pub updated_at: Option<String>,
88    pub snapshot_id: Option<String>,
89    #[serde(default)]
90    pub snapshot_taken_at_timestamp: Option<u64>,
91    #[serde(default)]
92    pub snapshot_total_size_bytes: Option<u64>,
93    pub artifact_path: Option<String>,
94    pub checksum: Option<String>,
95    pub failure_reason: Option<String>,
96}
97
98///
99/// BackupExecutionOperationReceiptOutcome
100///
101/// Terminal receipt outcome for one backup execution operation.
102/// Owned by backup execution and mapped back into operation state.
103///
104
105#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
106#[serde(rename_all = "kebab-case")]
107pub enum BackupExecutionOperationReceiptOutcome {
108    Completed,
109    Failed,
110    Skipped,
111}
112
113///
114/// BackupExecutionResumeSummary
115///
116/// Read-only summary of journal progress used by resume/reporting surfaces.
117/// Owned by backup execution and derived from current journal state.
118///
119
120#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
121#[serde(deny_unknown_fields)]
122pub struct BackupExecutionResumeSummary {
123    pub plan_id: String,
124    pub run_id: String,
125    pub preflight_id: Option<String>,
126    pub preflight_accepted: bool,
127    pub restart_required: bool,
128    pub total_operations: usize,
129    pub ready_operations: usize,
130    pub pending_operations: usize,
131    pub blocked_operations: usize,
132    pub completed_operations: usize,
133    pub failed_operations: usize,
134    pub skipped_operations: usize,
135    pub next_operation: Option<BackupExecutionJournalOperation>,
136}
137
138///
139/// BackupExecutionJournalError
140///
141/// Typed execution journal validation or transition failure.
142/// Owned by backup execution and returned before mutating invalid state.
143///
144
145#[derive(Debug, ThisError)]
146pub enum BackupExecutionJournalError {
147    #[error("invalid backup plan for execution journal: {0}")]
148    InvalidPlan(String),
149
150    #[error("unsupported backup execution journal version {0}")]
151    UnsupportedVersion(u16),
152
153    #[error("backup execution journal field {0} is required")]
154    MissingField(&'static str),
155
156    #[error("backup execution journal has duplicate operation sequence {0}")]
157    DuplicateSequence(usize),
158
159    #[error("backup execution journal is missing operation sequence {0}")]
160    MissingSequence(usize),
161
162    #[error("accepted preflight is missing preflight_id")]
163    AcceptedPreflightMissingId,
164
165    #[error("restart_required does not match execution operation state")]
166    RestartRequiredMismatch,
167
168    #[error("preflight already accepted as {existing}, cannot accept {attempted}")]
169    PreflightAlreadyAccepted { existing: String, attempted: String },
170
171    #[error("preflight receipt plan id {actual} does not match execution journal plan {expected}")]
172    PreflightPlanMismatch { expected: String, actual: String },
173
174    #[error("mutating operation {sequence} is ready before preflight acceptance")]
175    MutationReadyBeforePreflight { sequence: usize },
176
177    #[error("mutating operation {sequence} cannot run before preflight acceptance")]
178    MutationBeforePreflightAccepted { sequence: usize },
179
180    #[error("operation {0} is missing a blocking or failure reason")]
181    OperationMissingReason(usize),
182
183    #[error("operation {0} cannot have blocking reasons in its current state")]
184    UnblockedOperationHasReasons(usize),
185
186    #[error("operation {0} was not found")]
187    OperationNotFound(usize),
188
189    #[error("operation {sequence} cannot transition from {from:?} to {to:?}")]
190    InvalidOperationTransition {
191        sequence: usize,
192        from: BackupExecutionOperationState,
193        to: BackupExecutionOperationState,
194    },
195
196    #[error("operation {requested} cannot advance before operation {next}")]
197    OutOfOrderOperationTransition { requested: usize, next: usize },
198
199    #[error("no operation can be advanced")]
200    NoTransitionableOperation,
201
202    #[error("operation {0} is not failed")]
203    OperationNotFailed(usize),
204
205    #[error("operation receipt references missing operation {0}")]
206    ReceiptOperationNotFound(usize),
207
208    #[error("operation receipt does not match operation {sequence}")]
209    ReceiptOperationMismatch { sequence: usize },
210
211    #[error("operation receipt does not match journal {sequence}")]
212    ReceiptJournalMismatch { sequence: usize },
213
214    #[error("operation receipt does not match accepted preflight {sequence}")]
215    ReceiptPreflightMismatch { sequence: usize },
216
217    #[error("operation receipt {sequence} has no pending operation")]
218    ReceiptWithoutPendingOperation { sequence: usize },
219}