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},
18};
19use ic_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}
38impl<'workflow, 'request, 'source> IcSnapshotDataUploadPlan<'workflow, 'request, 'source> {
39 pub(crate) fn derive_kinds(
40 workflow: &ExecutionWorkflowRecord,
41 sequence: u64,
42 allocation: &IcSnapshotUploadReply<'_, '_>,
43 chunk_bytes: u64,
44 ) -> Result<Vec<SnapshotDataKind>, IcSnapshotDataUploadPlanningError> {
45 let metadata = allocation.request();
46 let stage = workflow.stage(sequence)?;
47 let IcSnapshotUploadReplyKind::Metadata { snapshot_id } = allocation.kind() else {
48 return Err(IcSnapshotDataUploadPlanningError::OriginalMismatch);
49 };
50 metadata.validate_data_destination(snapshot_id)?;
51 if stage.target() != metadata.target()
52 || !super::attempt::source_context_matches(workflow.allocation(), metadata)
53 {
54 return Err(IcSnapshotDataUploadPlanningError::OriginalMismatch);
55 }
56 Ok(planned_extents(
57 metadata.source(),
58 chunk_bytes,
59 stage.budget().mutations(),
60 )?)
61 }
62
63 pub(crate) fn from_bindings(
64 workflow: &'workflow ExecutionWorkflowRecord,
65 sequence: u64,
66 allocation: &IcSnapshotUploadReply<'request, 'source>,
67 chunk_bytes: u64,
68 bindings: &[ArtifactChecksumRecord],
69 ) -> Result<Self, IcSnapshotDataUploadPlanningError> {
70 let kinds = Self::derive_kinds(workflow, sequence, allocation, chunk_bytes)?;
71 if bindings.len() != kinds.len() {
72 return Err(IcSnapshotDataUploadPlanningError::OriginalMismatch);
73 }
74 let IcSnapshotUploadReplyKind::Metadata { snapshot_id } = allocation.kind() else {
75 return Err(IcSnapshotDataUploadPlanningError::OriginalMismatch);
76 };
77 let stage = workflow.stage(sequence)?;
78 let plan = data_stage_plan::build(workflow.allocation(), stage, bindings.iter().cloned())?;
79 Ok(Self {
80 workflow,
81 sequence,
82 metadata: allocation.request(),
83 destination: snapshot_id.clone(),
84 allocation_evidence: allocation.digest(),
85 kinds,
86 plan,
87 })
88 }
89
90 #[must_use]
92 pub fn kinds(&self) -> &[SnapshotDataKind] {
93 &self.kinds
94 }
95 #[must_use]
97 pub const fn metadata(&self) -> &'request IcSnapshotUploadRequest<'source> {
98 self.metadata
99 }
100 #[must_use]
102 pub fn destination(&self) -> &[u8] {
103 &self.destination
104 }
105 #[must_use]
107 pub const fn plan(&self) -> Option<&OperationPlanRecord> {
108 self.plan.as_ref()
109 }
110
111 pub fn bind(
119 &self,
120 allocation_binding: &ExecutionStageBindingRecord,
121 allocation_plan: &OperationPlanRecord,
122 predecessors: Vec<ExecutionStagePredecessorRecord>,
123 ) -> Result<ExecutionStageBindingRecord, IcSnapshotDataUploadPlanningError> {
124 let plan = self
125 .plan
126 .as_ref()
127 .ok_or(IcSnapshotDataUploadPlanningError::NoDataWrites)?;
128 allocation_binding.validate(self.workflow, allocation_plan)?;
129 if allocation_plan.operations().len() != 1
130 || allocation_plan.operations()[0].request() != self.metadata.binding_digest().hash()
131 || !predecessors.iter().any(|row| {
132 row.stage_sequence() == allocation_binding.stage_sequence()
133 && row.binding() == &allocation_binding.digest()
134 && row.learned_evidence() == &self.allocation_evidence
135 })
136 {
137 return Err(IcSnapshotDataUploadPlanningError::OriginalMismatch);
138 }
139 Ok(ExecutionStageBindingRecord::new(
140 self.workflow,
141 self.sequence,
142 plan,
143 predecessors,
144 )?)
145 }
146 pub(crate) fn validate_binding(
147 &self,
148 binding: &ExecutionStageBindingRecord,
149 ) -> Result<(), IcSnapshotDataUploadPlanningError> {
150 let plan = self
151 .plan
152 .as_ref()
153 .ok_or(IcSnapshotDataUploadPlanningError::NoDataWrites)?;
154 binding.validate(self.workflow, plan)?;
155 if binding.stage_sequence() != self.sequence
156 || !binding
157 .predecessors()
158 .iter()
159 .any(|row| row.learned_evidence() == &self.allocation_evidence)
160 {
161 return Err(IcSnapshotDataUploadPlanningError::OriginalMismatch);
162 }
163 Ok(())
164 }
165}
166
167#[derive(Debug, Error)]
169pub enum IcSnapshotDataUploadPlanningError {
170 #[error("invalid snapshot data upload chunk size")]
172 InvalidChunkSize,
173 #[error("snapshot data upload count overflow")]
175 CountOverflow,
176 #[error("snapshot data upload needs {required} updates; original allocation is {original}")]
178 InsufficientAllowance {
179 required: u64,
181 original: u32,
183 },
184 #[error("snapshot data upload originals differ")]
186 OriginalMismatch,
187 #[error("snapshot data upload has no data writes to bind")]
189 NoDataWrites,
190 #[error(transparent)]
192 Plan(#[from] OperationPlanError),
193 #[error(transparent)]
195 Graph(#[from] EffectGraphError),
196 #[error(transparent)]
198 Workflow(#[from] ExecutionWorkflowError),
199 #[error(transparent)]
201 Upload(#[from] IcSnapshotUploadError),
202 #[error(transparent)]
204 Budget(#[from] AttemptJournalRecordError),
205}
206impl From<ExtentPlanningError> for IcSnapshotDataUploadPlanningError {
207 fn from(error: ExtentPlanningError) -> Self {
208 match error {
209 ExtentPlanningError::InvalidChunkSize => Self::InvalidChunkSize,
210 ExtentPlanningError::CountOverflow => Self::CountOverflow,
211 ExtentPlanningError::InsufficientAllowance { required, original } => {
212 Self::InsufficientAllowance { required, original }
213 }
214 }
215 }
216}
217
218impl From<DataStagePlanError> for IcSnapshotDataUploadPlanningError {
219 fn from(error: DataStagePlanError) -> Self {
220 match error {
221 DataStagePlanError::CountOverflow => Self::CountOverflow,
222 DataStagePlanError::Graph(error) => Self::Graph(error),
223 DataStagePlanError::Plan(error) => Self::Plan(error),
224 DataStagePlanError::Budget(error) => Self::Budget(error),
225 }
226 }
227}
228
229#[cfg(test)]
230mod tests;