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