ic_backup/model/ic_snapshot_download/
mod.rs1use crate::model::{
4 attempt_journal::{AttemptBudgetRecord, AttemptJournalRecordError},
5 effect_graph::{EffectGraphError, EffectGraphRecord, EffectNodeRecord, EffectNodeRequest},
6 execution_workflow::{
7 ExecutionStageBindingRecord, ExecutionStagePredecessorRecord, ExecutionWorkflowError,
8 ExecutionWorkflowRecord,
9 },
10 ic_snapshot_data::{
11 IcSnapshotDataError, IcSnapshotDataRequest, MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES,
12 },
13 ic_snapshot_metadata::IcSnapshotMetadataReply,
14 operation_plan::{
15 OperationPlanError, OperationPlanRecord, OperationPlanRequest, PlanBudgetRecord,
16 PlannedOperationRecord, PlannedOperationRequest,
17 },
18};
19use ic_management_canister_types::SnapshotDataKind;
20use thiserror::Error;
21
22pub struct IcSnapshotDownloadPlan<'workflow, 'metadata> {
33 workflow: &'workflow ExecutionWorkflowRecord,
34 sequence: u64,
35 metadata: &'metadata IcSnapshotMetadataReply<'metadata>,
36 requests: Vec<IcSnapshotDataRequest<'metadata>>,
37 plan: Option<OperationPlanRecord>,
38}
39impl<'workflow, 'metadata> IcSnapshotDownloadPlan<'workflow, 'metadata> {
40 pub fn new(
49 workflow: &'workflow ExecutionWorkflowRecord,
50 sequence: u64,
51 metadata: &'metadata IcSnapshotMetadataReply<'metadata>,
52 chunk_bytes: u64,
53 ) -> Result<Self, IcSnapshotDownloadPlanningError> {
54 let stage = workflow.stage(sequence)?;
55 if stage.target() != metadata.request().target() {
56 return Err(IcSnapshotDownloadPlanningError::MetadataMismatch);
57 }
58 if chunk_bytes == 0 || chunk_bytes > MAX_IC_SNAPSHOT_DATA_CHUNK_BYTES as u64 {
59 return Err(IcSnapshotDownloadPlanningError::InvalidChunkSize);
60 }
61 let values = metadata.metadata();
62 let sizes = [
63 values.wasm_module_size,
64 values.wasm_memory_size,
65 values.stable_memory_size,
66 ];
67 let mut count = u64::try_from(values.wasm_chunk_store.len())
68 .map_err(|_| IcSnapshotDownloadPlanningError::CountOverflow)?;
69 for size in sizes {
70 let region = size / chunk_bytes + u64::from(size % chunk_bytes != 0);
71 count = count
72 .checked_add(region)
73 .ok_or(IcSnapshotDownloadPlanningError::CountOverflow)?;
74 }
75 if count > u64::from(stage.budget().mutations()) {
76 return Err(IcSnapshotDownloadPlanningError::InsufficientAllowance {
77 required: count,
78 original: stage.budget().mutations(),
79 });
80 }
81 let capacity =
82 usize::try_from(count).map_err(|_| IcSnapshotDownloadPlanningError::CountOverflow)?;
83 let mut requests = Vec::with_capacity(capacity);
84 for (region, total) in sizes.into_iter().enumerate() {
85 let mut offset = 0;
86 while offset < total {
87 let size = (total - offset).min(chunk_bytes);
88 let kind = match region {
89 0 => SnapshotDataKind::WasmModule { offset, size },
90 1 => SnapshotDataKind::WasmMemory { offset, size },
91 _ => SnapshotDataKind::StableMemory { offset, size },
92 };
93 requests.push(IcSnapshotDataRequest::new(metadata, kind)?);
94 offset += size; }
96 }
97 for chunk in &values.wasm_chunk_store {
98 requests.push(IcSnapshotDataRequest::new(
99 metadata,
100 SnapshotDataKind::WasmChunk {
101 hash: chunk.hash.clone(),
102 },
103 )?);
104 }
105 let plan = if requests.is_empty() {
106 None
107 } else {
108 let mut nodes = Vec::with_capacity(capacity);
109 let mut operations = Vec::with_capacity(capacity);
110 for (index, request) in requests.iter().enumerate() {
111 let sequence = u64::try_from(index)
112 .map_err(|_| IcSnapshotDownloadPlanningError::CountOverflow)?;
113 nodes.push(EffectNodeRecord::new(EffectNodeRequest {
114 operation_sequence: sequence,
115 depends_on: sequence.checked_sub(1).into_iter().collect(),
116 })?);
117 operations.push(PlannedOperationRecord::new(PlannedOperationRequest {
118 operation_sequence: sequence,
119 target: stage.target().into(),
120 request: request.digest().hash().into(),
121 budget: AttemptBudgetRecord::new(1, 0)?,
122 })?);
123 }
124 let original = workflow.allocation();
125 Some(OperationPlanRecord::new(OperationPlanRequest {
126 context: original.context().clone(),
127 inventory: original.inventory().clone(),
128 selected_targets: vec![stage.target().into()],
129 graph: EffectGraphRecord::new(nodes)?,
130 operations,
131 budget: PlanBudgetRecord::new(
132 stage.budget().mutations(),
133 stage.budget().observations(),
134 )?,
135 })?)
136 };
137 Ok(Self {
138 workflow,
139 sequence,
140 metadata,
141 requests,
142 plan,
143 })
144 }
145 #[must_use]
147 pub fn requests(&self) -> &[IcSnapshotDataRequest<'metadata>] {
148 &self.requests
149 }
150 #[must_use]
153 pub const fn plan(&self) -> Option<&OperationPlanRecord> {
154 self.plan.as_ref()
155 }
156 pub fn bind(
166 &self,
167 metadata_binding: &ExecutionStageBindingRecord,
168 metadata_plan: &OperationPlanRecord,
169 predecessors: Vec<ExecutionStagePredecessorRecord>,
170 ) -> Result<ExecutionStageBindingRecord, IcSnapshotDownloadPlanningError> {
171 let plan = self
172 .plan
173 .as_ref()
174 .ok_or(IcSnapshotDownloadPlanningError::NoDataReads)?;
175 metadata_binding.validate(self.workflow, metadata_plan)?;
176 if metadata_plan.operations().len() != 1
177 || metadata_plan.operations()[0].request() != self.metadata.request().digest().hash()
178 || !predecessors.iter().any(|row| {
179 row.stage_sequence() == metadata_binding.stage_sequence()
180 && row.binding() == &metadata_binding.digest()
181 && row.learned_evidence() == &self.metadata.digest()
182 })
183 {
184 return Err(IcSnapshotDownloadPlanningError::MetadataMismatch);
185 }
186 Ok(ExecutionStageBindingRecord::new(
187 self.workflow,
188 self.sequence,
189 plan,
190 predecessors,
191 )?)
192 }
193}
194
195#[derive(Debug, Error)]
197pub enum IcSnapshotDownloadPlanningError {
198 #[error("invalid snapshot download chunk size")]
200 InvalidChunkSize,
201 #[error("snapshot download request count overflow")]
203 CountOverflow,
204 #[error("snapshot download needs {required} updates; original allocation is {original}")]
206 InsufficientAllowance {
207 required: u64,
209 original: u32,
211 },
212 #[error("snapshot download metadata differs from original stage")]
214 MetadataMismatch,
215 #[error("snapshot download has no data reads to bind")]
217 NoDataReads,
218 #[error(transparent)]
220 Plan(#[from] OperationPlanError),
221 #[error(transparent)]
223 Graph(#[from] EffectGraphError),
224 #[error(transparent)]
226 Workflow(#[from] ExecutionWorkflowError),
227 #[error(transparent)]
229 Data(#[from] IcSnapshotDataError),
230 #[error(transparent)]
232 Budget(#[from] AttemptJournalRecordError),
233}
234
235#[cfg(test)]
236mod tests;