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