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