ic_backup/model/ic_snapshot_upload/plan/
mod.rs1use super::{
4 IcSnapshotUploadError, IcSnapshotUploadReply, IcSnapshotUploadReplyKind,
5 IcSnapshotUploadRequest,
6};
7use crate::model::{
8 artifacts::ArtifactChecksumRecord,
9 attempt_journal::AttemptJournalRecordError,
10 data_stage_plan::{self, DataStagePlanError, LearnedDataStage, LearnedDataStageError},
11 effect_graph::EffectGraphError,
12 execution_workflow::{
13 ExecutionStageBindingRecord, ExecutionStagePredecessorRecord, ExecutionWorkflowError,
14 ExecutionWorkflowRecord,
15 },
16 ic_snapshot_data::{ExtentPlanningError, planned_extents},
17 operation_plan::{OperationPlanError, OperationPlanRecord, PlannedOperationRecord},
18};
19use ic_platform::management_canister_types::SnapshotDataKind;
20use thiserror::Error;
21
22pub struct IcSnapshotDataUploadPlan<'workflow, 'request, 'source> {
30 binding: LearnedDataStage<'workflow>,
31 metadata: &'request IcSnapshotUploadRequest<'source>,
32 destination: Vec<u8>,
33 writes: DataWrites,
34}
35
36#[expect(
38 clippy::large_enum_variant,
39 reason = "keep the original plan inline rather than add a heap allocation to every nonempty upload"
40)]
41enum DataWrites {
42 Empty,
43 Writes {
44 kinds: Vec<SnapshotDataKind>,
45 plan: OperationPlanRecord,
46 },
47}
48#[derive(Clone)]
50pub(crate) enum UploadCompletion {
51 Data,
52 Allocation {
53 sequence: u64,
54 evidence: ArtifactChecksumRecord,
55 },
56}
57
58pub(crate) struct DataUploadPreparation<'workflow, 'request, 'source> {
61 workflow: &'workflow ExecutionWorkflowRecord,
62 stage: &'workflow PlannedOperationRecord,
63 metadata: &'request IcSnapshotUploadRequest<'source>,
64 destination: Vec<u8>,
65 allocation_evidence: ArtifactChecksumRecord,
66 kinds: Vec<SnapshotDataKind>,
67}
68
69impl<'workflow, 'request, 'source> DataUploadPreparation<'workflow, 'request, 'source> {
70 pub(crate) fn new(
71 workflow: &'workflow ExecutionWorkflowRecord,
72 sequence: u64,
73 allocation: &IcSnapshotUploadReply<'request, 'source>,
74 chunk_bytes: u64,
75 ) -> Result<Self, IcSnapshotDataUploadPlanningError> {
76 let metadata = allocation.request();
77 let stage = workflow.stage(sequence)?;
78 let IcSnapshotUploadReplyKind::Metadata { snapshot_id } = allocation.kind() else {
79 return Err(IcSnapshotDataUploadPlanningError::OriginalMismatch);
80 };
81 metadata.validate_data_destination(snapshot_id)?;
82 if stage.target() != metadata.target()
83 || !super::attempt::source_context_matches(workflow.allocation(), metadata)
84 {
85 return Err(IcSnapshotDataUploadPlanningError::OriginalMismatch);
86 }
87 let kinds = planned_extents(metadata.source(), chunk_bytes, stage.budget().mutations())?;
88 Ok(Self {
89 workflow,
90 stage,
91 metadata,
92 destination: snapshot_id.clone(),
93 allocation_evidence: allocation.digest(),
94 kinds,
95 })
96 }
97
98 pub(crate) const fn metadata(&self) -> &'request IcSnapshotUploadRequest<'source> {
99 self.metadata
100 }
101 pub(crate) fn destination(&self) -> &[u8] {
102 &self.destination
103 }
104 pub(crate) fn kinds(&self) -> &[SnapshotDataKind] {
105 &self.kinds
106 }
107
108 pub(crate) fn finish(
109 self,
110 bindings: &[ArtifactChecksumRecord],
111 ) -> Result<
112 IcSnapshotDataUploadPlan<'workflow, 'request, 'source>,
113 IcSnapshotDataUploadPlanningError,
114 > {
115 if bindings.len() != self.kinds.len() {
116 return Err(IcSnapshotDataUploadPlanningError::OriginalMismatch);
117 }
118 let plan = data_stage_plan::build(
119 self.workflow.allocation(),
120 self.stage,
121 bindings.iter().cloned(),
122 )?;
123 Ok(IcSnapshotDataUploadPlan {
124 binding: LearnedDataStage {
125 workflow: self.workflow,
126 sequence: self.stage.operation_sequence(),
127 evidence: self.allocation_evidence,
128 },
129 metadata: self.metadata,
130 destination: self.destination,
131 writes: match plan {
132 Some(plan) => DataWrites::Writes {
133 kinds: self.kinds,
134 plan,
135 },
136 None => DataWrites::Empty,
137 },
138 })
139 }
140}
141
142impl<'request, 'source> IcSnapshotDataUploadPlan<'_, 'request, 'source> {
143 pub(crate) fn workflow_digest(&self) -> ArtifactChecksumRecord {
144 self.binding.workflow.digest()
145 }
146 pub(crate) fn validate_completion(
147 &self,
148 binding: &ExecutionStageBindingRecord,
149 plan: &OperationPlanRecord,
150 learned: &ArtifactChecksumRecord,
151 ) -> Result<UploadCompletion, IcSnapshotDataUploadPlanningError> {
152 if let Some(expected) = self.plan() {
153 self.validate_binding(binding)?;
154 if plan != expected {
155 return Err(IcSnapshotDataUploadPlanningError::OriginalMismatch);
156 }
157 Ok(UploadCompletion::Data)
158 } else {
159 binding.validate(self.binding.workflow, plan)?;
160 let [operation] = plan.operations() else {
161 return Err(IcSnapshotDataUploadPlanningError::OriginalMismatch);
162 };
163 if operation.target() != self.metadata.target()
164 || operation.request() != self.metadata.binding_digest().hash()
165 || learned != &self.binding.evidence
166 {
167 return Err(IcSnapshotDataUploadPlanningError::OriginalMismatch);
168 }
169 Ok(UploadCompletion::Allocation {
170 sequence: operation.operation_sequence(),
171 evidence: self.binding.evidence.clone(),
172 })
173 }
174 }
175 #[must_use]
177 pub fn kinds(&self) -> &[SnapshotDataKind] {
178 match &self.writes {
179 DataWrites::Empty => &[],
180 DataWrites::Writes { kinds, .. } => kinds,
181 }
182 }
183 #[must_use]
185 pub const fn metadata(&self) -> &'request IcSnapshotUploadRequest<'source> {
186 self.metadata
187 }
188 #[must_use]
190 pub fn destination(&self) -> &[u8] {
191 &self.destination
192 }
193 #[must_use]
195 pub const fn plan(&self) -> Option<&OperationPlanRecord> {
196 match &self.writes {
197 DataWrites::Empty => None,
198 DataWrites::Writes { plan, .. } => Some(plan),
199 }
200 }
201
202 pub fn bind(
210 &self,
211 allocation_binding: &ExecutionStageBindingRecord,
212 allocation_plan: &OperationPlanRecord,
213 predecessors: Vec<ExecutionStagePredecessorRecord>,
214 ) -> Result<ExecutionStageBindingRecord, IcSnapshotDataUploadPlanningError> {
215 let plan = self
216 .plan()
217 .ok_or(IcSnapshotDataUploadPlanningError::NoDataWrites)?;
218 self.binding
219 .bind(
220 plan,
221 allocation_binding,
222 allocation_plan,
223 self.metadata.binding_digest().hash(),
224 predecessors,
225 )
226 .map_err(Into::into)
227 }
228 pub(crate) fn validate_binding(
229 &self,
230 binding: &ExecutionStageBindingRecord,
231 ) -> Result<(), IcSnapshotDataUploadPlanningError> {
232 let plan = self
233 .plan()
234 .ok_or(IcSnapshotDataUploadPlanningError::NoDataWrites)?;
235 Ok(self.binding.validate(binding, plan)?)
236 }
237}
238
239#[derive(Debug, Error)]
241pub enum IcSnapshotDataUploadPlanningError {
242 #[error("invalid snapshot data upload chunk size")]
244 InvalidChunkSize,
245 #[error("snapshot data upload count overflow")]
247 CountOverflow,
248 #[error("snapshot data upload needs {required} updates; original allocation is {original}")]
250 InsufficientAllowance {
251 required: u64,
253 original: u32,
255 },
256 #[error("snapshot data upload originals differ")]
258 OriginalMismatch,
259 #[error("snapshot data upload has no data writes to bind")]
261 NoDataWrites,
262 #[error(transparent)]
264 Plan(#[from] OperationPlanError),
265 #[error(transparent)]
267 Graph(#[from] EffectGraphError),
268 #[error(transparent)]
270 Workflow(#[from] ExecutionWorkflowError),
271 #[error(transparent)]
273 Upload(#[from] IcSnapshotUploadError),
274 #[error(transparent)]
276 Budget(#[from] AttemptJournalRecordError),
277}
278impl From<ExtentPlanningError> for IcSnapshotDataUploadPlanningError {
279 fn from(error: ExtentPlanningError) -> Self {
280 match error {
281 ExtentPlanningError::InvalidChunkSize => Self::InvalidChunkSize,
282 ExtentPlanningError::CountOverflow => Self::CountOverflow,
283 ExtentPlanningError::InsufficientAllowance { required, original } => {
284 Self::InsufficientAllowance { required, original }
285 }
286 }
287 }
288}
289
290impl From<DataStagePlanError> for IcSnapshotDataUploadPlanningError {
291 fn from(error: DataStagePlanError) -> Self {
292 match error {
293 DataStagePlanError::CountOverflow => Self::CountOverflow,
294 DataStagePlanError::Graph(error) => Self::Graph(error),
295 DataStagePlanError::Plan(error) => Self::Plan(error),
296 DataStagePlanError::Budget(error) => Self::Budget(error),
297 }
298 }
299}
300
301impl From<LearnedDataStageError> for IcSnapshotDataUploadPlanningError {
302 fn from(error: LearnedDataStageError) -> Self {
303 match error {
304 LearnedDataStageError::Workflow(error) => Self::Workflow(error),
305 LearnedDataStageError::Mismatch => Self::OriginalMismatch,
306 }
307 }
308}
309
310#[cfg(test)]
311mod tests;