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