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},
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 workflow: &'workflow ExecutionWorkflowRecord,
31 sequence: u64,
32 metadata: &'request IcSnapshotUploadRequest<'source>,
33 destination: Vec<u8>,
34 allocation_evidence: ArtifactChecksumRecord,
35 kinds: Vec<SnapshotDataKind>,
36 plan: Option<OperationPlanRecord>,
37}
38pub(crate) struct DataUploadPreparation<'workflow, 'request, 'source> {
41 workflow: &'workflow ExecutionWorkflowRecord,
42 stage: &'workflow PlannedOperationRecord,
43 metadata: &'request IcSnapshotUploadRequest<'source>,
44 destination: Vec<u8>,
45 allocation_evidence: ArtifactChecksumRecord,
46 kinds: Vec<SnapshotDataKind>,
47}
48
49impl<'workflow, 'request, 'source> DataUploadPreparation<'workflow, 'request, 'source> {
50 pub(crate) fn new(
51 workflow: &'workflow ExecutionWorkflowRecord,
52 sequence: u64,
53 allocation: &IcSnapshotUploadReply<'request, 'source>,
54 chunk_bytes: u64,
55 ) -> Result<Self, IcSnapshotDataUploadPlanningError> {
56 let metadata = allocation.request();
57 let stage = workflow.stage(sequence)?;
58 let IcSnapshotUploadReplyKind::Metadata { snapshot_id } = allocation.kind() else {
59 return Err(IcSnapshotDataUploadPlanningError::OriginalMismatch);
60 };
61 metadata.validate_data_destination(snapshot_id)?;
62 if stage.target() != metadata.target()
63 || !super::attempt::source_context_matches(workflow.allocation(), metadata)
64 {
65 return Err(IcSnapshotDataUploadPlanningError::OriginalMismatch);
66 }
67 let kinds = planned_extents(metadata.source(), chunk_bytes, stage.budget().mutations())?;
68 Ok(Self {
69 workflow,
70 stage,
71 metadata,
72 destination: snapshot_id.clone(),
73 allocation_evidence: allocation.digest(),
74 kinds,
75 })
76 }
77
78 pub(crate) const fn metadata(&self) -> &'request IcSnapshotUploadRequest<'source> {
79 self.metadata
80 }
81 pub(crate) fn destination(&self) -> &[u8] {
82 &self.destination
83 }
84 pub(crate) fn kinds(&self) -> &[SnapshotDataKind] {
85 &self.kinds
86 }
87
88 pub(crate) fn finish(
89 self,
90 bindings: &[ArtifactChecksumRecord],
91 ) -> Result<
92 IcSnapshotDataUploadPlan<'workflow, 'request, 'source>,
93 IcSnapshotDataUploadPlanningError,
94 > {
95 if bindings.len() != self.kinds.len() {
96 return Err(IcSnapshotDataUploadPlanningError::OriginalMismatch);
97 }
98 let plan = data_stage_plan::build(
99 self.workflow.allocation(),
100 self.stage,
101 bindings.iter().cloned(),
102 )?;
103 Ok(IcSnapshotDataUploadPlan {
104 workflow: self.workflow,
105 sequence: self.stage.operation_sequence(),
106 metadata: self.metadata,
107 destination: self.destination,
108 allocation_evidence: self.allocation_evidence,
109 kinds: self.kinds,
110 plan,
111 })
112 }
113}
114
115impl<'request, 'source> IcSnapshotDataUploadPlan<'_, 'request, 'source> {
116 pub(crate) fn workflow_digest(&self) -> ArtifactChecksumRecord {
117 self.workflow.digest()
118 }
119 pub(crate) fn validate_completion(
120 &self,
121 binding: &ExecutionStageBindingRecord,
122 plan: &OperationPlanRecord,
123 learned: &ArtifactChecksumRecord,
124 ) -> Result<(), IcSnapshotDataUploadPlanningError> {
125 if let Some(expected) = self.plan() {
126 self.validate_binding(binding)?;
127 if plan != expected {
128 return Err(IcSnapshotDataUploadPlanningError::OriginalMismatch);
129 }
130 } else {
131 binding.validate(self.workflow, plan)?;
132 if plan.operations().len() != 1
133 || plan.operations()[0].target() != self.metadata.target()
134 || plan.operations()[0].request() != self.metadata.binding_digest().hash()
135 || learned != &self.allocation_evidence
136 {
137 return Err(IcSnapshotDataUploadPlanningError::OriginalMismatch);
138 }
139 }
140 Ok(())
141 }
142 #[must_use]
144 pub fn kinds(&self) -> &[SnapshotDataKind] {
145 &self.kinds
146 }
147 #[must_use]
149 pub const fn metadata(&self) -> &'request IcSnapshotUploadRequest<'source> {
150 self.metadata
151 }
152 #[must_use]
154 pub fn destination(&self) -> &[u8] {
155 &self.destination
156 }
157 #[must_use]
159 pub const fn plan(&self) -> Option<&OperationPlanRecord> {
160 self.plan.as_ref()
161 }
162
163 pub fn bind(
171 &self,
172 allocation_binding: &ExecutionStageBindingRecord,
173 allocation_plan: &OperationPlanRecord,
174 predecessors: Vec<ExecutionStagePredecessorRecord>,
175 ) -> Result<ExecutionStageBindingRecord, IcSnapshotDataUploadPlanningError> {
176 let plan = self
177 .plan
178 .as_ref()
179 .ok_or(IcSnapshotDataUploadPlanningError::NoDataWrites)?;
180 allocation_binding.validate(self.workflow, allocation_plan)?;
181 if !allocation_binding.matches_learned_predecessor(
182 allocation_plan,
183 self.metadata.binding_digest().hash(),
184 &self.allocation_evidence,
185 &predecessors,
186 ) {
187 return Err(IcSnapshotDataUploadPlanningError::OriginalMismatch);
188 }
189 Ok(ExecutionStageBindingRecord::new(
190 self.workflow,
191 self.sequence,
192 plan,
193 predecessors,
194 )?)
195 }
196 pub(crate) fn validate_binding(
197 &self,
198 binding: &ExecutionStageBindingRecord,
199 ) -> Result<(), IcSnapshotDataUploadPlanningError> {
200 let plan = self
201 .plan
202 .as_ref()
203 .ok_or(IcSnapshotDataUploadPlanningError::NoDataWrites)?;
204 binding.validate(self.workflow, plan)?;
205 if !binding.has_learned_evidence(self.sequence, &self.allocation_evidence) {
206 return Err(IcSnapshotDataUploadPlanningError::OriginalMismatch);
207 }
208 Ok(())
209 }
210}
211
212#[derive(Debug, Error)]
214pub enum IcSnapshotDataUploadPlanningError {
215 #[error("invalid snapshot data upload chunk size")]
217 InvalidChunkSize,
218 #[error("snapshot data upload count overflow")]
220 CountOverflow,
221 #[error("snapshot data upload needs {required} updates; original allocation is {original}")]
223 InsufficientAllowance {
224 required: u64,
226 original: u32,
228 },
229 #[error("snapshot data upload originals differ")]
231 OriginalMismatch,
232 #[error("snapshot data upload has no data writes to bind")]
234 NoDataWrites,
235 #[error(transparent)]
237 Plan(#[from] OperationPlanError),
238 #[error(transparent)]
240 Graph(#[from] EffectGraphError),
241 #[error(transparent)]
243 Workflow(#[from] ExecutionWorkflowError),
244 #[error(transparent)]
246 Upload(#[from] IcSnapshotUploadError),
247 #[error(transparent)]
249 Budget(#[from] AttemptJournalRecordError),
250}
251impl From<ExtentPlanningError> for IcSnapshotDataUploadPlanningError {
252 fn from(error: ExtentPlanningError) -> Self {
253 match error {
254 ExtentPlanningError::InvalidChunkSize => Self::InvalidChunkSize,
255 ExtentPlanningError::CountOverflow => Self::CountOverflow,
256 ExtentPlanningError::InsufficientAllowance { required, original } => {
257 Self::InsufficientAllowance { required, original }
258 }
259 }
260 }
261}
262
263impl From<DataStagePlanError> for IcSnapshotDataUploadPlanningError {
264 fn from(error: DataStagePlanError) -> Self {
265 match error {
266 DataStagePlanError::CountOverflow => Self::CountOverflow,
267 DataStagePlanError::Graph(error) => Self::Graph(error),
268 DataStagePlanError::Plan(error) => Self::Plan(error),
269 DataStagePlanError::Budget(error) => Self::Budget(error),
270 }
271 }
272}
273
274#[cfg(test)]
275mod tests;