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},
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    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}
38// Carry the admitted allocation and bounded extents across guarded source IO.
39// Only finish can turn their exact ordered byte bindings into a public plan.
40pub(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    /// Read exact ordered source extents. Operation sequences are zero-based indices.
117    #[must_use]
118    pub fn kinds(&self) -> &[SnapshotDataKind] {
119        &self.kinds
120    }
121    /// Read the original metadata allocation declaration and retained source association.
122    #[must_use]
123    pub const fn metadata(&self) -> &'request IcSnapshotUploadRequest<'source> {
124        self.metadata
125    }
126    /// Read exact declared destination bytes; they grant no allocation authority.
127    #[must_use]
128    pub fn destination(&self) -> &[u8] {
129        &self.destination
130    }
131    /// Read the immutable child plan, or None when there are no data writes.
132    #[must_use]
133    pub const fn plan(&self) -> Option<&OperationPlanRecord> {
134        self.plan.as_ref()
135    }
136
137    /// Bind the exact original singleton allocation and request/raw-reply evidence.
138    ///
139    /// Existing stage persistence requires complete Applied predecessor journals and
140    /// their chronological checkpoint. This check authenticates neither allocation
141    /// attribution nor byte custody and grants no dispatch/load/start authority.
142    /// # Errors
143    /// Rejects an empty plan, changed allocation/request/evidence or original dependency.
144    pub fn bind(
145        &self,
146        allocation_binding: &ExecutionStageBindingRecord,
147        allocation_plan: &OperationPlanRecord,
148        predecessors: Vec<ExecutionStagePredecessorRecord>,
149    ) -> Result<ExecutionStageBindingRecord, IcSnapshotDataUploadPlanningError> {
150        let plan = self
151            .plan
152            .as_ref()
153            .ok_or(IcSnapshotDataUploadPlanningError::NoDataWrites)?;
154        allocation_binding.validate(self.workflow, allocation_plan)?;
155        if !allocation_binding.matches_learned_predecessor(
156            allocation_plan,
157            self.metadata.binding_digest().hash(),
158            &self.allocation_evidence,
159            &predecessors,
160        ) {
161            return Err(IcSnapshotDataUploadPlanningError::OriginalMismatch);
162        }
163        Ok(ExecutionStageBindingRecord::new(
164            self.workflow,
165            self.sequence,
166            plan,
167            predecessors,
168        )?)
169    }
170    pub(crate) fn validate_binding(
171        &self,
172        binding: &ExecutionStageBindingRecord,
173    ) -> Result<(), IcSnapshotDataUploadPlanningError> {
174        let plan = self
175            .plan
176            .as_ref()
177            .ok_or(IcSnapshotDataUploadPlanningError::NoDataWrites)?;
178        binding.validate(self.workflow, plan)?;
179        if !binding.has_learned_evidence(self.sequence, &self.allocation_evidence) {
180            return Err(IcSnapshotDataUploadPlanningError::OriginalMismatch);
181        }
182        Ok(())
183    }
184}
185
186/// Planning rejection changes no source bytes, journal, allowance or remote effects.
187#[derive(Debug, Error)]
188pub enum IcSnapshotDataUploadPlanningError {
189    /// Chunk size is outside the existing 1..=1 MiB data boundary.
190    #[error("invalid snapshot data upload chunk size")]
191    InvalidChunkSize,
192    /// Complete declared extent counts overflow nat64 or host indexing.
193    #[error("snapshot data upload count overflow")]
194    CountOverflow,
195    /// Complete data writes cannot fit their original stage allocation.
196    #[error("snapshot data upload needs {required} updates; original allocation is {original}")]
197    InsufficientAllowance {
198        /// Complete required update count, excluding metadata allocation.
199        required: u64,
200        /// Original data-stage ceiling, excluding unassigned workflow headroom.
201        original: u32,
202    },
203    /// Original context, source, allocation, request or predecessor evidence differs.
204    #[error("snapshot data upload originals differ")]
205    OriginalMismatch,
206    /// Empty data has no placeholder child plan or checkpoint.
207    #[error("snapshot data upload has no data writes to bind")]
208    NoDataWrites,
209    /// Existing immutable operation plan admission.
210    #[error(transparent)]
211    Plan(#[from] OperationPlanError),
212    /// Existing explicit graph admission.
213    #[error(transparent)]
214    Graph(#[from] EffectGraphError),
215    /// Existing original stage binding admission.
216    #[error(transparent)]
217    Workflow(#[from] ExecutionWorkflowError),
218    /// Existing exact source/destination codec admission.
219    #[error(transparent)]
220    Upload(#[from] IcSnapshotUploadError),
221    /// Existing original per-operation attempt admission.
222    #[error(transparent)]
223    Budget(#[from] AttemptJournalRecordError),
224}
225impl From<ExtentPlanningError> for IcSnapshotDataUploadPlanningError {
226    fn from(error: ExtentPlanningError) -> Self {
227        match error {
228            ExtentPlanningError::InvalidChunkSize => Self::InvalidChunkSize,
229            ExtentPlanningError::CountOverflow => Self::CountOverflow,
230            ExtentPlanningError::InsufficientAllowance { required, original } => {
231                Self::InsufficientAllowance { required, original }
232            }
233        }
234    }
235}
236
237impl From<DataStagePlanError> for IcSnapshotDataUploadPlanningError {
238    fn from(error: DataStagePlanError) -> Self {
239        match error {
240            DataStagePlanError::CountOverflow => Self::CountOverflow,
241            DataStagePlanError::Graph(error) => Self::Graph(error),
242            DataStagePlanError::Plan(error) => Self::Plan(error),
243            DataStagePlanError::Budget(error) => Self::Budget(error),
244        }
245    }
246}
247
248#[cfg(test)]
249mod tests;