Skip to main content

ic_backup/model/ic_snapshot_download/
mod.rs

1//! Bounded metadata-derived exact download plans; no calls, spending or transfer proof.
2
3use crate::model::{
4    attempt_journal::AttemptJournalRecordError,
5    data_stage_plan::{self, DataStagePlanError},
6    effect_graph::EffectGraphError,
7    execution_workflow::{
8        ExecutionStageBindingRecord, ExecutionStagePredecessorRecord, ExecutionWorkflowError,
9        ExecutionWorkflowRecord,
10    },
11    ic_snapshot_data::{
12        ExtentPlanningError, IcSnapshotDataError, IcSnapshotDataRequest, planned_extents,
13    },
14    ic_snapshot_metadata::IcSnapshotMetadataReply,
15    operation_plan::{OperationPlanError, OperationPlanRecord},
16};
17use thiserror::Error;
18
19#[cfg(test)]
20use crate::model::{
21    attempt_journal::AttemptBudgetRecord,
22    operation_plan::{PlanBudgetRecord, PlannedOperationRecord, PlannedOperationRequest},
23};
24
25/// Exact finite data requests and their original allocation-bound ordinary child plan.
26///
27/// Requests cover module, heap and stable regions in that order, then every known
28/// chunk in original metadata order. Empty regions need no requests; even empty
29/// known chunks require a hash-checked reply. Opaque operation IDs are zero-based
30/// ordinals with explicit sequential dependencies for the contiguous writer.
31/// Every request has one mutation-lane replicated update and zero recovery calls.
32/// Lost replies therefore stop with pending spending; this is not a retry schedule.
33/// The child aggregate retains original stage ceilings and leaves spare allowance
34/// unassigned. An entirely read-free snapshot has no child plan or binding.
35pub struct IcSnapshotDownloadPlan<'workflow, 'metadata> {
36    workflow: &'workflow ExecutionWorkflowRecord,
37    sequence: u64,
38    metadata: &'metadata IcSnapshotMetadataReply<'metadata>,
39    requests: Vec<IcSnapshotDataRequest<'metadata>>,
40    plan: Option<OperationPlanRecord>,
41}
42impl<'workflow, 'metadata> IcSnapshotDownloadPlan<'workflow, 'metadata> {
43    /// Derive exact data payloads within the original target and mutation allocation.
44    ///
45    /// Count checked region ceilings and known chunks before allocating/encoding
46    /// requests. No iteration or allocation grows with an unadmitted remote nat64.
47    /// Fresh access, authentic metadata/extent custody and execution remain separate.
48    /// # Errors
49    /// Rejects unknown/wrong stages, invalid chunks, arithmetic overflow, insufficient
50    /// original allowance and existing bounded request/plan admission failures.
51    pub fn new(
52        workflow: &'workflow ExecutionWorkflowRecord,
53        sequence: u64,
54        metadata: &'metadata IcSnapshotMetadataReply<'metadata>,
55        chunk_bytes: u64,
56    ) -> Result<Self, IcSnapshotDownloadPlanningError> {
57        let stage = workflow.stage(sequence)?;
58        if stage.target() != metadata.request().target() {
59            return Err(IcSnapshotDownloadPlanningError::MetadataMismatch);
60        }
61        let kinds = planned_extents(metadata, chunk_bytes, stage.budget().mutations())
62            .map_err(IcSnapshotDownloadPlanningError::from)?;
63        let requests = kinds
64            .into_iter()
65            .map(|kind| IcSnapshotDataRequest::new(metadata, kind))
66            .collect::<Result<Vec<_>, _>>()?;
67        let plan = data_stage_plan::build(
68            workflow.allocation(),
69            stage,
70            requests.iter().map(IcSnapshotDataRequest::digest),
71        )?;
72        Ok(Self {
73            workflow,
74            sequence,
75            metadata,
76            requests,
77            plan,
78        })
79    }
80    /// Read exact ordered requests; operation IDs are their zero-based indices.
81    #[must_use]
82    pub fn requests(&self) -> &[IcSnapshotDataRequest<'metadata>] {
83        &self.requests
84    }
85    // Every immutable request was constructed from this same original metadata.
86    pub(crate) fn matches_metadata(&self, metadata: &IcSnapshotMetadataReply<'_>) -> bool {
87        self.metadata.digest() == metadata.digest()
88    }
89    /// Read the original-allocation child plan. None means no data calls are needed,
90    /// not that capture, durable publication or complete transfer is qualified.
91    #[must_use]
92    pub const fn plan(&self) -> Option<&OperationPlanRecord> {
93        self.plan.as_ref()
94    }
95
96    /// Re-admit an exact retained data-stage binding and its original metadata evidence.
97    /// This structural association grants no fresh permission or transfer attestation.
98    pub(crate) fn validate_binding(
99        &self,
100        binding: &ExecutionStageBindingRecord,
101    ) -> Result<(), IcSnapshotDownloadPlanningError> {
102        let plan = self
103            .plan
104            .as_ref()
105            .ok_or(IcSnapshotDownloadPlanningError::NoDataReads)?;
106        binding.validate(self.workflow, plan)?;
107        if !binding.has_learned_evidence(self.sequence, &self.metadata.digest()) {
108            return Err(IcSnapshotDownloadPlanningError::MetadataMismatch);
109        }
110        Ok(())
111    }
112    /// Bind the exact original metadata stage and reply evidence before stage creation.
113    ///
114    /// The source stage must contain exactly one metadata request under this original
115    /// workflow. Its direct predecessor row must retain its binding and this exact
116    /// metadata request/raw-reply evidence digest. Existing stage persistence still
117    /// admits the complete Applied source journals and settlement. These checks do
118    /// not authenticate metadata or prove that an opaque receipt describes its reply.
119    /// # Errors
120    /// Rejects a read-free plan, wrong metadata stage/plan/input or original dependencies.
121    pub fn bind(
122        &self,
123        metadata_binding: &ExecutionStageBindingRecord,
124        metadata_plan: &OperationPlanRecord,
125        predecessors: Vec<ExecutionStagePredecessorRecord>,
126    ) -> Result<ExecutionStageBindingRecord, IcSnapshotDownloadPlanningError> {
127        let plan = self
128            .plan
129            .as_ref()
130            .ok_or(IcSnapshotDownloadPlanningError::NoDataReads)?;
131        metadata_binding.validate(self.workflow, metadata_plan)?;
132        if !metadata_binding.matches_learned_predecessor(
133            metadata_plan,
134            self.metadata.request().digest().hash(),
135            &self.metadata.digest(),
136            &predecessors,
137        ) {
138            return Err(IcSnapshotDownloadPlanningError::MetadataMismatch);
139        }
140        Ok(ExecutionStageBindingRecord::new(
141            self.workflow,
142            self.sequence,
143            plan,
144            predecessors,
145        )?)
146    }
147}
148
149/// Pure planning failure; no filesystem, provider, reservation or receipt changes.
150#[derive(Debug, Error)]
151pub enum IcSnapshotDownloadPlanningError {
152    /// Chunk size is outside the existing 1..=1 MiB request boundary.
153    #[error("invalid snapshot download chunk size")]
154    InvalidChunkSize,
155    /// Combined remote declared region counts cannot fit nat64.
156    #[error("snapshot download request count overflow")]
157    CountOverflow,
158    /// Original stage cannot pay for every exact required data request.
159    #[error("snapshot download needs {required} updates; original allocation is {original}")]
160    InsufficientAllowance {
161        /// Complete exact required update count.
162        required: u64,
163        /// Original stage mutation ceiling, excluding workflow headroom.
164        original: u32,
165    },
166    /// Target, original metadata stage/request or exact reply evidence changed.
167    #[error("snapshot download metadata differs from original stage")]
168    MetadataMismatch,
169    /// There is no data call to bind; no placeholder operation is created.
170    #[error("snapshot download has no data reads to bind")]
171    NoDataReads,
172    /// Canonical original plan or allocation rejected.
173    #[error(transparent)]
174    Plan(#[from] OperationPlanError),
175    /// Existing explicit graph owner rejected.
176    #[error(transparent)]
177    Graph(#[from] EffectGraphError),
178    /// Existing immutable stage owner rejected.
179    #[error(transparent)]
180    Workflow(#[from] ExecutionWorkflowError),
181    /// Existing exact metadata-bound data request owner rejected.
182    #[error(transparent)]
183    Data(#[from] IcSnapshotDataError),
184    /// Existing per-operation budget owner rejected.
185    #[error(transparent)]
186    Budget(#[from] AttemptJournalRecordError),
187}
188
189#[cfg(test)]
190pub(crate) mod tests;
191
192impl From<ExtentPlanningError> for IcSnapshotDownloadPlanningError {
193    fn from(error: ExtentPlanningError) -> Self {
194        match error {
195            ExtentPlanningError::InvalidChunkSize => Self::InvalidChunkSize,
196            ExtentPlanningError::CountOverflow => Self::CountOverflow,
197            ExtentPlanningError::InsufficientAllowance { required, original } => {
198                Self::InsufficientAllowance { required, original }
199            }
200        }
201    }
202}
203
204impl From<DataStagePlanError> for IcSnapshotDownloadPlanningError {
205    fn from(error: DataStagePlanError) -> Self {
206        match error {
207            DataStagePlanError::CountOverflow => Self::CountOverflow,
208            DataStagePlanError::Graph(error) => Self::Graph(error),
209            DataStagePlanError::Plan(error) => Self::Plan(error),
210            DataStagePlanError::Budget(error) => Self::Budget(error),
211        }
212    }
213}