Skip to main content

ic_backup/model/ic_snapshot_upload/plan/
mod.rs

1//! Exact bounded data-upload declarations, retaining no aggregate payload bytes.
2
3use 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},
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
22/// Complete ordered extents and source-bound request digests for one original data stage.
23///
24/// Guarded local source preparation creates this declaration. Module, heap and stable
25/// regions precede known chunks in original metadata order. Every operation has one
26/// update and zero observations with an explicit predecessor dependency. Original
27/// stage headroom stays unassigned. No upload bytes, spending, authenticated allocation
28/// or complete-transfer proof are retained here. Empty data needs no placeholder plan.
29pub struct IcSnapshotDataUploadPlan<'workflow, 'request, 'source> {
30    binding: LearnedDataStage<'workflow>,
31    metadata: &'request IcSnapshotUploadRequest<'source>,
32    destination: Vec<u8>,
33    kinds: Vec<SnapshotDataKind>,
34    plan: Option<OperationPlanRecord>,
35}
36// Carry the admitted allocation and bounded extents across guarded source IO.
37// Only finish can turn their exact ordered byte bindings into a public plan.
38pub(crate) struct DataUploadPreparation<'workflow, 'request, 'source> {
39    workflow: &'workflow ExecutionWorkflowRecord,
40    stage: &'workflow PlannedOperationRecord,
41    metadata: &'request IcSnapshotUploadRequest<'source>,
42    destination: Vec<u8>,
43    allocation_evidence: ArtifactChecksumRecord,
44    kinds: Vec<SnapshotDataKind>,
45}
46
47impl<'workflow, 'request, 'source> DataUploadPreparation<'workflow, 'request, 'source> {
48    pub(crate) fn new(
49        workflow: &'workflow ExecutionWorkflowRecord,
50        sequence: u64,
51        allocation: &IcSnapshotUploadReply<'request, 'source>,
52        chunk_bytes: u64,
53    ) -> Result<Self, IcSnapshotDataUploadPlanningError> {
54        let metadata = allocation.request();
55        let stage = workflow.stage(sequence)?;
56        let IcSnapshotUploadReplyKind::Metadata { snapshot_id } = allocation.kind() else {
57            return Err(IcSnapshotDataUploadPlanningError::OriginalMismatch);
58        };
59        metadata.validate_data_destination(snapshot_id)?;
60        if stage.target() != metadata.target()
61            || !super::attempt::source_context_matches(workflow.allocation(), metadata)
62        {
63            return Err(IcSnapshotDataUploadPlanningError::OriginalMismatch);
64        }
65        let kinds = planned_extents(metadata.source(), chunk_bytes, stage.budget().mutations())?;
66        Ok(Self {
67            workflow,
68            stage,
69            metadata,
70            destination: snapshot_id.clone(),
71            allocation_evidence: allocation.digest(),
72            kinds,
73        })
74    }
75
76    pub(crate) const fn metadata(&self) -> &'request IcSnapshotUploadRequest<'source> {
77        self.metadata
78    }
79    pub(crate) fn destination(&self) -> &[u8] {
80        &self.destination
81    }
82    pub(crate) fn kinds(&self) -> &[SnapshotDataKind] {
83        &self.kinds
84    }
85
86    pub(crate) fn finish(
87        self,
88        bindings: &[ArtifactChecksumRecord],
89    ) -> Result<
90        IcSnapshotDataUploadPlan<'workflow, 'request, 'source>,
91        IcSnapshotDataUploadPlanningError,
92    > {
93        if bindings.len() != self.kinds.len() {
94            return Err(IcSnapshotDataUploadPlanningError::OriginalMismatch);
95        }
96        let plan = data_stage_plan::build(
97            self.workflow.allocation(),
98            self.stage,
99            bindings.iter().cloned(),
100        )?;
101        Ok(IcSnapshotDataUploadPlan {
102            binding: LearnedDataStage {
103                workflow: self.workflow,
104                sequence: self.stage.operation_sequence(),
105                evidence: self.allocation_evidence,
106            },
107            metadata: self.metadata,
108            destination: self.destination,
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.binding.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.binding.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.binding.evidence
136            {
137                return Err(IcSnapshotDataUploadPlanningError::OriginalMismatch);
138            }
139        }
140        Ok(())
141    }
142    /// Read exact ordered source extents. Operation sequences are zero-based indices.
143    #[must_use]
144    pub fn kinds(&self) -> &[SnapshotDataKind] {
145        &self.kinds
146    }
147    /// Read the original metadata allocation declaration and retained source association.
148    #[must_use]
149    pub const fn metadata(&self) -> &'request IcSnapshotUploadRequest<'source> {
150        self.metadata
151    }
152    /// Read exact declared destination bytes; they grant no allocation authority.
153    #[must_use]
154    pub fn destination(&self) -> &[u8] {
155        &self.destination
156    }
157    /// Read the immutable child plan, or None when there are no data writes.
158    #[must_use]
159    pub const fn plan(&self) -> Option<&OperationPlanRecord> {
160        self.plan.as_ref()
161    }
162
163    /// Bind the exact original singleton allocation and request/raw-reply evidence.
164    ///
165    /// Existing stage persistence requires complete Applied predecessor journals and
166    /// their chronological checkpoint. This check authenticates neither allocation
167    /// attribution nor byte custody and grants no dispatch/load/start authority.
168    /// # Errors
169    /// Rejects an empty plan, changed allocation/request/evidence or original dependency.
170    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        self.binding
181            .bind(
182                plan,
183                allocation_binding,
184                allocation_plan,
185                self.metadata.binding_digest().hash(),
186                predecessors,
187            )?
188            .ok_or(IcSnapshotDataUploadPlanningError::OriginalMismatch)
189    }
190    pub(crate) fn validate_binding(
191        &self,
192        binding: &ExecutionStageBindingRecord,
193    ) -> Result<(), IcSnapshotDataUploadPlanningError> {
194        let plan = self
195            .plan
196            .as_ref()
197            .ok_or(IcSnapshotDataUploadPlanningError::NoDataWrites)?;
198        if !self.binding.validate(binding, plan)? {
199            return Err(IcSnapshotDataUploadPlanningError::OriginalMismatch);
200        }
201        Ok(())
202    }
203}
204
205/// Planning rejection changes no source bytes, journal, allowance or remote effects.
206#[derive(Debug, Error)]
207pub enum IcSnapshotDataUploadPlanningError {
208    /// Chunk size is outside the existing 1..=1 MiB data boundary.
209    #[error("invalid snapshot data upload chunk size")]
210    InvalidChunkSize,
211    /// Complete declared extent counts overflow nat64 or host indexing.
212    #[error("snapshot data upload count overflow")]
213    CountOverflow,
214    /// Complete data writes cannot fit their original stage allocation.
215    #[error("snapshot data upload needs {required} updates; original allocation is {original}")]
216    InsufficientAllowance {
217        /// Complete required update count, excluding metadata allocation.
218        required: u64,
219        /// Original data-stage ceiling, excluding unassigned workflow headroom.
220        original: u32,
221    },
222    /// Original context, source, allocation, request or predecessor evidence differs.
223    #[error("snapshot data upload originals differ")]
224    OriginalMismatch,
225    /// Empty data has no placeholder child plan or checkpoint.
226    #[error("snapshot data upload has no data writes to bind")]
227    NoDataWrites,
228    /// Existing immutable operation plan admission.
229    #[error(transparent)]
230    Plan(#[from] OperationPlanError),
231    /// Existing explicit graph admission.
232    #[error(transparent)]
233    Graph(#[from] EffectGraphError),
234    /// Existing original stage binding admission.
235    #[error(transparent)]
236    Workflow(#[from] ExecutionWorkflowError),
237    /// Existing exact source/destination codec admission.
238    #[error(transparent)]
239    Upload(#[from] IcSnapshotUploadError),
240    /// Existing original per-operation attempt admission.
241    #[error(transparent)]
242    Budget(#[from] AttemptJournalRecordError),
243}
244impl From<ExtentPlanningError> for IcSnapshotDataUploadPlanningError {
245    fn from(error: ExtentPlanningError) -> Self {
246        match error {
247            ExtentPlanningError::InvalidChunkSize => Self::InvalidChunkSize,
248            ExtentPlanningError::CountOverflow => Self::CountOverflow,
249            ExtentPlanningError::InsufficientAllowance { required, original } => {
250                Self::InsufficientAllowance { required, original }
251            }
252        }
253    }
254}
255
256impl From<DataStagePlanError> for IcSnapshotDataUploadPlanningError {
257    fn from(error: DataStagePlanError) -> Self {
258        match error {
259            DataStagePlanError::CountOverflow => Self::CountOverflow,
260            DataStagePlanError::Graph(error) => Self::Graph(error),
261            DataStagePlanError::Plan(error) => Self::Plan(error),
262            DataStagePlanError::Budget(error) => Self::Budget(error),
263        }
264    }
265}
266
267#[cfg(test)]
268mod tests;