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    /// Read the original-allocation child plan. None means no data calls are needed,
86    /// not that capture, durable publication or complete transfer is qualified.
87    #[must_use]
88    pub const fn plan(&self) -> Option<&OperationPlanRecord> {
89        self.plan.as_ref()
90    }
91
92    /// Re-admit an exact retained data-stage binding and its original metadata evidence.
93    /// This structural association grants no fresh permission or transfer attestation.
94    pub(crate) fn validate_binding(
95        &self,
96        binding: &ExecutionStageBindingRecord,
97    ) -> Result<(), IcSnapshotDownloadPlanningError> {
98        let plan = self
99            .plan
100            .as_ref()
101            .ok_or(IcSnapshotDownloadPlanningError::NoDataReads)?;
102        binding.validate(self.workflow, plan)?;
103        if !binding.has_learned_evidence(self.sequence, &self.metadata.digest()) {
104            return Err(IcSnapshotDownloadPlanningError::MetadataMismatch);
105        }
106        Ok(())
107    }
108    /// Bind the exact original metadata stage and reply evidence before stage creation.
109    ///
110    /// The source stage must contain exactly one metadata request under this original
111    /// workflow. Its direct predecessor row must retain its binding and this exact
112    /// metadata request/raw-reply evidence digest. Existing stage persistence still
113    /// admits the complete Applied source journals and settlement. These checks do
114    /// not authenticate metadata or prove that an opaque receipt describes its reply.
115    /// # Errors
116    /// Rejects a read-free plan, wrong metadata stage/plan/input or original dependencies.
117    pub fn bind(
118        &self,
119        metadata_binding: &ExecutionStageBindingRecord,
120        metadata_plan: &OperationPlanRecord,
121        predecessors: Vec<ExecutionStagePredecessorRecord>,
122    ) -> Result<ExecutionStageBindingRecord, IcSnapshotDownloadPlanningError> {
123        let plan = self
124            .plan
125            .as_ref()
126            .ok_or(IcSnapshotDownloadPlanningError::NoDataReads)?;
127        metadata_binding.validate(self.workflow, metadata_plan)?;
128        if !metadata_binding.matches_learned_predecessor(
129            metadata_plan,
130            self.metadata.request().digest().hash(),
131            &self.metadata.digest(),
132            &predecessors,
133        ) {
134            return Err(IcSnapshotDownloadPlanningError::MetadataMismatch);
135        }
136        Ok(ExecutionStageBindingRecord::new(
137            self.workflow,
138            self.sequence,
139            plan,
140            predecessors,
141        )?)
142    }
143}
144
145/// Pure planning failure; no filesystem, provider, reservation or receipt changes.
146#[derive(Debug, Error)]
147pub enum IcSnapshotDownloadPlanningError {
148    /// Chunk size is outside the existing 1..=1 MiB request boundary.
149    #[error("invalid snapshot download chunk size")]
150    InvalidChunkSize,
151    /// Combined remote declared region counts cannot fit nat64.
152    #[error("snapshot download request count overflow")]
153    CountOverflow,
154    /// Original stage cannot pay for every exact required data request.
155    #[error("snapshot download needs {required} updates; original allocation is {original}")]
156    InsufficientAllowance {
157        /// Complete exact required update count.
158        required: u64,
159        /// Original stage mutation ceiling, excluding workflow headroom.
160        original: u32,
161    },
162    /// Target, original metadata stage/request or exact reply evidence changed.
163    #[error("snapshot download metadata differs from original stage")]
164    MetadataMismatch,
165    /// There is no data call to bind; no placeholder operation is created.
166    #[error("snapshot download has no data reads to bind")]
167    NoDataReads,
168    /// Canonical original plan or allocation rejected.
169    #[error(transparent)]
170    Plan(#[from] OperationPlanError),
171    /// Existing explicit graph owner rejected.
172    #[error(transparent)]
173    Graph(#[from] EffectGraphError),
174    /// Existing immutable stage owner rejected.
175    #[error(transparent)]
176    Workflow(#[from] ExecutionWorkflowError),
177    /// Existing exact metadata-bound data request owner rejected.
178    #[error(transparent)]
179    Data(#[from] IcSnapshotDataError),
180    /// Existing per-operation budget owner rejected.
181    #[error(transparent)]
182    Budget(#[from] AttemptJournalRecordError),
183}
184
185#[cfg(test)]
186pub(crate) mod tests;
187
188impl From<ExtentPlanningError> for IcSnapshotDownloadPlanningError {
189    fn from(error: ExtentPlanningError) -> Self {
190        match error {
191            ExtentPlanningError::InvalidChunkSize => Self::InvalidChunkSize,
192            ExtentPlanningError::CountOverflow => Self::CountOverflow,
193            ExtentPlanningError::InsufficientAllowance { required, original } => {
194                Self::InsufficientAllowance { required, original }
195            }
196        }
197    }
198}
199
200impl From<DataStagePlanError> for IcSnapshotDownloadPlanningError {
201    fn from(error: DataStagePlanError) -> Self {
202        match error {
203            DataStagePlanError::CountOverflow => Self::CountOverflow,
204            DataStagePlanError::Graph(error) => Self::Graph(error),
205            DataStagePlanError::Plan(error) => Self::Plan(error),
206            DataStagePlanError::Budget(error) => Self::Budget(error),
207        }
208    }
209}