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},
18};
19use ic_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}
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        kinds: Vec<SnapshotDataKind>,
68        bindings: &[ArtifactChecksumRecord],
69    ) -> Result<Self, IcSnapshotDataUploadPlanningError> {
70        // Consume the unchanged extents admitted before source IO. Workflow and
71        // allocation are the same immutable owners used by `derive_kinds`.
72        if bindings.len() != kinds.len() {
73            return Err(IcSnapshotDataUploadPlanningError::OriginalMismatch);
74        }
75        let IcSnapshotUploadReplyKind::Metadata { snapshot_id } = allocation.kind() else {
76            return Err(IcSnapshotDataUploadPlanningError::OriginalMismatch);
77        };
78        let stage = workflow.stage(sequence)?;
79        let plan = data_stage_plan::build(workflow.allocation(), stage, bindings.iter().cloned())?;
80        Ok(Self {
81            workflow,
82            sequence,
83            metadata: allocation.request(),
84            destination: snapshot_id.clone(),
85            allocation_evidence: allocation.digest(),
86            kinds,
87            plan,
88        })
89    }
90
91    /// Read exact ordered source extents. Operation sequences are zero-based indices.
92    #[must_use]
93    pub fn kinds(&self) -> &[SnapshotDataKind] {
94        &self.kinds
95    }
96    /// Read the original metadata allocation declaration and retained source association.
97    #[must_use]
98    pub const fn metadata(&self) -> &'request IcSnapshotUploadRequest<'source> {
99        self.metadata
100    }
101    /// Read exact declared destination bytes; they grant no allocation authority.
102    #[must_use]
103    pub fn destination(&self) -> &[u8] {
104        &self.destination
105    }
106    /// Read the immutable child plan, or None when there are no data writes.
107    #[must_use]
108    pub const fn plan(&self) -> Option<&OperationPlanRecord> {
109        self.plan.as_ref()
110    }
111
112    /// Bind the exact original singleton allocation and request/raw-reply evidence.
113    ///
114    /// Existing stage persistence requires complete Applied predecessor journals and
115    /// their chronological checkpoint. This check authenticates neither allocation
116    /// attribution nor byte custody and grants no dispatch/load/start authority.
117    /// # Errors
118    /// Rejects an empty plan, changed allocation/request/evidence or original dependency.
119    pub fn bind(
120        &self,
121        allocation_binding: &ExecutionStageBindingRecord,
122        allocation_plan: &OperationPlanRecord,
123        predecessors: Vec<ExecutionStagePredecessorRecord>,
124    ) -> Result<ExecutionStageBindingRecord, IcSnapshotDataUploadPlanningError> {
125        let plan = self
126            .plan
127            .as_ref()
128            .ok_or(IcSnapshotDataUploadPlanningError::NoDataWrites)?;
129        allocation_binding.validate(self.workflow, allocation_plan)?;
130        if allocation_plan.operations().len() != 1
131            || allocation_plan.operations()[0].request() != self.metadata.binding_digest().hash()
132            || !predecessors.iter().any(|row| {
133                row.stage_sequence() == allocation_binding.stage_sequence()
134                    && row.binding() == &allocation_binding.digest()
135                    && row.learned_evidence() == &self.allocation_evidence
136            })
137        {
138            return Err(IcSnapshotDataUploadPlanningError::OriginalMismatch);
139        }
140        Ok(ExecutionStageBindingRecord::new(
141            self.workflow,
142            self.sequence,
143            plan,
144            predecessors,
145        )?)
146    }
147    pub(crate) fn validate_binding(
148        &self,
149        binding: &ExecutionStageBindingRecord,
150    ) -> Result<(), IcSnapshotDataUploadPlanningError> {
151        let plan = self
152            .plan
153            .as_ref()
154            .ok_or(IcSnapshotDataUploadPlanningError::NoDataWrites)?;
155        binding.validate(self.workflow, plan)?;
156        if binding.stage_sequence() != self.sequence
157            || !binding
158                .predecessors()
159                .iter()
160                .any(|row| row.learned_evidence() == &self.allocation_evidence)
161        {
162            return Err(IcSnapshotDataUploadPlanningError::OriginalMismatch);
163        }
164        Ok(())
165    }
166}
167
168/// Planning rejection changes no source bytes, journal, allowance or remote effects.
169#[derive(Debug, Error)]
170pub enum IcSnapshotDataUploadPlanningError {
171    /// Chunk size is outside the existing 1..=1 MiB data boundary.
172    #[error("invalid snapshot data upload chunk size")]
173    InvalidChunkSize,
174    /// Complete declared extent counts overflow nat64 or host indexing.
175    #[error("snapshot data upload count overflow")]
176    CountOverflow,
177    /// Complete data writes cannot fit their original stage allocation.
178    #[error("snapshot data upload needs {required} updates; original allocation is {original}")]
179    InsufficientAllowance {
180        /// Complete required update count, excluding metadata allocation.
181        required: u64,
182        /// Original data-stage ceiling, excluding unassigned workflow headroom.
183        original: u32,
184    },
185    /// Original context, source, allocation, request or predecessor evidence differs.
186    #[error("snapshot data upload originals differ")]
187    OriginalMismatch,
188    /// Empty data has no placeholder child plan or checkpoint.
189    #[error("snapshot data upload has no data writes to bind")]
190    NoDataWrites,
191    /// Existing immutable operation plan admission.
192    #[error(transparent)]
193    Plan(#[from] OperationPlanError),
194    /// Existing explicit graph admission.
195    #[error(transparent)]
196    Graph(#[from] EffectGraphError),
197    /// Existing original stage binding admission.
198    #[error(transparent)]
199    Workflow(#[from] ExecutionWorkflowError),
200    /// Existing exact source/destination codec admission.
201    #[error(transparent)]
202    Upload(#[from] IcSnapshotUploadError),
203    /// Existing original per-operation attempt admission.
204    #[error(transparent)]
205    Budget(#[from] AttemptJournalRecordError),
206}
207impl From<ExtentPlanningError> for IcSnapshotDataUploadPlanningError {
208    fn from(error: ExtentPlanningError) -> Self {
209        match error {
210            ExtentPlanningError::InvalidChunkSize => Self::InvalidChunkSize,
211            ExtentPlanningError::CountOverflow => Self::CountOverflow,
212            ExtentPlanningError::InsufficientAllowance { required, original } => {
213                Self::InsufficientAllowance { required, original }
214            }
215        }
216    }
217}
218
219impl From<DataStagePlanError> for IcSnapshotDataUploadPlanningError {
220    fn from(error: DataStagePlanError) -> Self {
221        match error {
222            DataStagePlanError::CountOverflow => Self::CountOverflow,
223            DataStagePlanError::Graph(error) => Self::Graph(error),
224            DataStagePlanError::Plan(error) => Self::Plan(error),
225            DataStagePlanError::Budget(error) => Self::Budget(error),
226        }
227    }
228}
229
230#[cfg(test)]
231mod tests;