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