Skip to main content

ic_backup/workflow/ic_snapshot_download/
mod.rs

1//! Complete original planned data transfer with explicit integration-qualified receipts.
2
3use crate::model::data_stage_plan::SnapshotDataPlanningError;
4use crate::{
5    model::{
6        artifacts::ArtifactChecksumRecord,
7        attempt_journal::MutationReceiptRequest,
8        ic_snapshot_download::IcSnapshotDownloadPlan,
9        ic_snapshot_transfer_read::{
10            IcSnapshotTransferReadPayload, IcSnapshotTransferReadRequest,
11            IcSnapshotTransferReadResponse,
12        },
13    },
14    ops::persistence::{
15        AttemptJournalError, AttemptJournalGuard, ExecutionProgressPersistenceError,
16        ExecutionStageGuard, ExecutionWorkflowPersistenceError, IcSnapshotArtifactError,
17        IcSnapshotArtifactWriter, read_execution_progress,
18    },
19    policy::ic_snapshot_transfer_read::{IcSnapshotTransferReadReply, IcSnapshotTransferReadView},
20    ports::ic_snapshot_transfer_read::IcSnapshotTransferReadProvider,
21    workflow::ic_snapshot_transfer_read::{
22        IcSnapshotTransferReadExecutionError, read_snapshot_with,
23    },
24};
25use thiserror::Error;
26
27/// Stream every original data request into an existing private writer and publish it.
28///
29/// The metadata read and its independently qualified receipt/checkpoint must already
30/// exist. Prepare the metadata-derived data stage and its complete original journals,
31/// retain exact token/raw-ID association, then create the writer with exact raw metadata.
32/// Hold no attempt guards on entry. Read-free plans and any consumed original attempt
33/// reject; this is a fresh transfer, never partial-read resume or artifact recovery.
34///
35/// For every request, `admit` qualifies fresh access, metadata/command custody and
36/// application requirements under the existing single-read coordinator. `qualify`
37/// must independently authenticate exact original response attribution and return an
38/// explicit original Applied receipt. Wire shape alone cannot qualify that receipt.
39/// It runs under the same original journal lock held through dispatch and reuses
40/// that read's decoded reply. Append exact decoded bytes before
41/// the sole journal owner records that receipt; only then may the next dependent read
42/// run. Failed qualification, append or persistence stops without another provider call.
43/// Admission, submission and qualification are awaited under their selected journal
44/// guards. Retain each reply durably before cancellable qualification work. Dropping
45/// this future consumes the writer, retaining partial bytes, earlier receipts and
46/// the current pending reservation; it grants no resume or repeat-call authority.
47///
48/// All original limits/records remain unchanged. Errors consume the writer and preserve
49/// partial bytes, pending spending and every recorded receipt. Returned bounded replies
50/// survive post-read rejection. Success uses the existing fresh checksum/durable
51/// publisher, but supplies no immutable manifest/checkpoint, complete product backup,
52/// terminal proof, default provider, refund or fence/reference release. Stable bytes,
53/// complete authenticated backend transfer and application safety stay integration-owned.
54/// # Errors
55/// Rejects changed stage/plan/metadata/writer, prior consumption, spending, admission,
56/// provider or receipt failures, malformed replies and local IO/publication failure.
57pub async fn download_snapshot<E: std::error::Error + 'static>(
58    stage: &ExecutionStageGuard<'_>,
59    download: &IcSnapshotDownloadPlan<'_, '_>,
60    mut writer: IcSnapshotArtifactWriter<'_, '_, '_>,
61    provider: &mut impl IcSnapshotTransferReadProvider,
62    mut admit: impl AsyncFnMut(&IcSnapshotTransferReadRequest<'_, '_>) -> Result<(), E>,
63    mut qualify: impl AsyncFnMut(
64        &IcSnapshotTransferReadRequest<'_, '_>,
65        &IcSnapshotTransferReadResponse,
66    ) -> Result<MutationReceiptRequest, E>,
67) -> Result<ArtifactChecksumRecord, IcSnapshotDownloadExecutionError<E>> {
68    download.validate_binding(stage.binding())?;
69    let plan = download
70        .plan()
71        .ok_or(SnapshotDataPlanningError::NoDataOperations)?;
72    if stage.plan() != plan
73        || !download.matches_metadata(writer.coverage().metadata())
74        || writer.coverage().covered_region_bytes() != [0; 3]
75        || writer.coverage().covered_chunks() != 0
76    {
77        return Err(IcSnapshotDownloadExecutionError::OriginalMismatch);
78    }
79    let intent = plan.digest();
80    writer.validate_transfer_origin(stage.layout()?.root(), intent.hash())?;
81    let progress = read_execution_progress(stage.layout()?, &intent)?;
82    if !progress.is_unattempted() {
83        return Err(IcSnapshotDownloadExecutionError::AlreadyAttempted);
84    }
85    // The plan constructor pairs each request with one ordered operation.
86    for (operation, payload) in plan.operations().iter().zip(download.requests()) {
87        let sequence = operation.operation_sequence();
88        let payload = payload.map_err(SnapshotDataPlanningError::from)?;
89        let (response, result) = read_snapshot_with(
90            stage,
91            sequence,
92            IcSnapshotTransferReadPayload::Data(&payload),
93            provider,
94            &mut admit,
95            async |journal, request, admitted| {
96                append_qualified(stage, journal, request, admitted, writer, &mut qualify).await
97            },
98        )
99        .await?;
100        writer = result.map_err(|source| IcSnapshotDownloadExecutionError::AfterReply {
101            operation_sequence: sequence,
102            source,
103            response: Box::new(response),
104        })?;
105    }
106    let progress = read_execution_progress(stage.layout()?, &intent)?;
107    if !progress.all_applied() {
108        return Err(IcSnapshotDownloadExecutionError::AlreadyAttempted);
109    }
110    writer.validate_transfer_origin(stage.layout()?.root(), intent.hash())?;
111    let checksum = writer.finish()?;
112    if let Err(source) = stage.layout() {
113        return Err(IcSnapshotDownloadExecutionError::AfterPublication { source, checksum });
114    }
115    Ok(checksum)
116}
117
118async fn append_qualified<'journal, 'layout, 'metadata, E: std::error::Error + 'static>(
119    stage: &ExecutionStageGuard<'_>,
120    journal: &mut AttemptJournalGuard<'_>,
121    request: &IcSnapshotTransferReadRequest<'_, '_>,
122    admitted: &IcSnapshotTransferReadView<'_, '_>,
123    writer: IcSnapshotArtifactWriter<'journal, 'layout, 'metadata>,
124    qualify: &mut impl AsyncFnMut(
125        &IcSnapshotTransferReadRequest<'_, '_>,
126        &IcSnapshotTransferReadResponse,
127    ) -> Result<MutationReceiptRequest, E>,
128) -> Result<IcSnapshotArtifactWriter<'journal, 'layout, 'metadata>, IcSnapshotDownloadReplyError<E>>
129{
130    let receipt = qualify(request, admitted.response())
131        .await
132        .map_err(IcSnapshotDownloadReplyError::Qualification)?;
133    let writer = super::qualified_receipt::record_applied(
134        journal,
135        receipt,
136        request.mutation_attempt(),
137        request.payload().digest().hash(),
138        IcSnapshotDownloadReplyError::ReceiptRequired,
139        || {
140            // The sealed request authority already binds this admitted stage plan.
141            writer.validate_transfer_origin(
142                stage.layout()?.root(),
143                request.authority().binding().intent(),
144            )?;
145            let IcSnapshotTransferReadReply::Data(reply) = admitted.reply() else {
146                return Err(IcSnapshotDownloadReplyError::ReceiptRequired);
147            };
148            Ok(writer.append(reply)?)
149        },
150    )?;
151    stage.layout()?;
152    Ok(writer)
153}
154
155/// Failures preserve original spending and partial/published artifact evidence.
156#[derive(Debug, Error)]
157pub enum IcSnapshotDownloadExecutionError<E: std::error::Error + 'static> {
158    /// Exact retained writer, stage or metadata differs.
159    #[error("snapshot download originals differ")]
160    OriginalMismatch,
161    /// A prior attempt cannot be replayed by this fresh transfer entrypoint.
162    #[error("snapshot download original data stage was already attempted")]
163    AlreadyAttempted,
164    /// Existing original download plan/binding admission.
165    #[error(transparent)]
166    Planning(#[from] SnapshotDataPlanningError),
167    /// Existing retained stage/ancestor admission.
168    #[error(transparent)]
169    Stage(#[from] ExecutionWorkflowPersistenceError),
170    /// Complete original progress admission.
171    #[error(transparent)]
172    Progress(#[from] ExecutionProgressPersistenceError),
173    /// One original read failed; its owner retains any returned bounded reply.
174    #[error(transparent)]
175    Read(#[from] IcSnapshotTransferReadExecutionError<E>),
176    /// Canonical writer/checksum/durable publication failure.
177    #[error(transparent)]
178    Artifact(#[from] IcSnapshotArtifactError),
179    /// Rejection after a returned read retains the exact bounded response.
180    #[error("snapshot download reply rejected for operation {operation_sequence}: {source}")]
181    AfterReply {
182        /// Exact original data operation.
183        operation_sequence: u64,
184        /// Original rejection without undoing receipt or spending.
185        source: IcSnapshotDownloadReplyError<E>,
186        /// Exact bounded returned bytes and passive claims.
187        response: Box<IcSnapshotTransferReadResponse>,
188    },
189    /// Publication succeeded but stage re-admission failed; retain the published checksum.
190    #[error("snapshot download stage changed after publication: {source}")]
191    AfterPublication {
192        /// Retained stage/ancestor admission failure.
193        source: ExecutionWorkflowPersistenceError,
194        /// Existing durable publisher's returned checksum.
195        checksum: ArtifactChecksumRecord,
196    },
197}
198
199/// Rejection of a returned data reply while retaining its original attempt.
200#[derive(Debug, Error)]
201pub enum IcSnapshotDownloadReplyError<E: std::error::Error + 'static> {
202    /// Fresh authenticated exact attribution was not qualified by the integration.
203    #[error("snapshot download reply qualification failed: {0}")]
204    Qualification(#[source] E),
205    /// Require the explicit exact original Applied receipt, without outcome inference.
206    #[error("snapshot download requires an explicit original Applied receipt")]
207    ReceiptRequired,
208    /// Existing original stage admission.
209    #[error(transparent)]
210    Stage(#[from] ExecutionWorkflowPersistenceError),
211    /// Original selected journal/spending/receipt admission.
212    #[error(transparent)]
213    Journal(#[from] AttemptJournalError),
214    /// Exact local bytes/custody admission failed.
215    #[error(transparent)]
216    Artifact(#[from] IcSnapshotArtifactError),
217}
218
219#[cfg(test)]
220mod tests;