Skip to main content

ic_backup/workflow/ic_snapshot_metadata/
mod.rs

1//! One original metadata stage, with explicit qualified receipt and learned evidence.
2
3use crate::{
4    model::{
5        artifacts::ArtifactChecksumRecord,
6        attempt_journal::MutationReceiptRequest,
7        ic_snapshot_metadata::IcSnapshotMetadataRequest,
8        ic_snapshot_transfer_read::{
9            IcSnapshotTransferReadPayload, IcSnapshotTransferReadRequest,
10            IcSnapshotTransferReadResponse,
11        },
12    },
13    ops::persistence::{
14        AttemptJournalError, AttemptJournalGuard, ExecutionStageGuard,
15        ExecutionWorkflowPersistenceError,
16    },
17    policy::ic_snapshot_transfer_read::{IcSnapshotTransferReadReply, IcSnapshotTransferReadView},
18    ports::ic_snapshot_transfer_read::IcSnapshotTransferReadProvider,
19    workflow::ic_snapshot_transfer_read::{
20        IcSnapshotTransferReadExecutionError, read_snapshot_with,
21    },
22};
23use thiserror::Error;
24
25/// Read one new exact singleton metadata stage and record its qualified receipt.
26///
27/// Prepare the original stage and complete journals first; hold no attempt guards on
28/// entry. Reuse the canonical single-read owner for durable spending, mandatory fresh
29/// admission and exactly one provider call. A pending or previously Applied read is
30/// never dispatched again, including after interruption before checkpoint publication.
31///
32/// `admit` owns fresh read permission, raw-ID/application and never-dispatched custody.
33/// `qualify` runs under the original journal lock and must independently authenticate
34/// exact response attribution AND durably retain original request/reply bytes before
35/// returning the explicit Applied receipt. There is no default qualification or byte
36/// store. Opaque provider evidence and valid wire shape cannot substitute for it.
37/// Account any remote qualification separately before its calls.
38/// Admission, submission and independent qualification are async. Qualification
39/// holds the same original journal continuously from dispatch, reuses its decoded
40/// reply, and must retain the reply durably before
41/// awaiting cancellable work. Cancellation keeps pending/Applied spending and denies
42/// reentry; no executor or `Send` bound is imposed by the core.
43///
44/// Record the receipt through the sole spending owner, then return the response and
45/// canonical request/raw-metadata digest. The workflow owner checkpoints that learned
46/// evidence after journal exclusion ends. Direct callers use the same stage checkpoint
47/// owner explicitly before binding successors. Preparing data stages remains separate.
48/// No data calls, artifact, default authority, terminal proof or reference release follows.
49/// # Errors
50/// Rejects non-singleton or changed originals, spending, fresh admission, provider,
51/// association, qualification and receipt failures without cleanup/refund.
52pub async fn read_snapshot_metadata<E: std::error::Error + 'static>(
53    stage: &ExecutionStageGuard<'_>,
54    operation_sequence: u64,
55    payload: &IcSnapshotMetadataRequest,
56    provider: &mut impl IcSnapshotTransferReadProvider,
57    admit: impl AsyncFnOnce(&IcSnapshotTransferReadRequest<'_, '_>) -> Result<(), E>,
58    qualify: impl AsyncFnOnce(
59        &IcSnapshotTransferReadRequest<'_, '_>,
60        &IcSnapshotTransferReadResponse,
61    ) -> Result<MutationReceiptRequest, E>,
62) -> Result<
63    (IcSnapshotTransferReadResponse, ArtifactChecksumRecord),
64    IcSnapshotMetadataExecutionError<E>,
65> {
66    let operation = stage
67        .plan()
68        .singleton_operation(operation_sequence)
69        .ok_or(IcSnapshotMetadataExecutionError::OriginalMismatch)?;
70    if operation.request() != payload.digest().hash() {
71        return Err(IcSnapshotMetadataExecutionError::OriginalMismatch);
72    }
73    let (response, settlement) = read_snapshot_with(
74        stage,
75        operation_sequence,
76        IcSnapshotTransferReadPayload::Metadata(payload),
77        provider,
78        admit,
79        async |journal, request, admitted| {
80            settle_metadata(stage, journal, request, admitted, qualify).await
81        },
82    )
83    .await?;
84    super::qualified_receipt::retain_reply(response, settlement, |source, response| {
85        IcSnapshotMetadataExecutionError::AfterReply { source, response }
86    })
87}
88
89async fn settle_metadata<E: std::error::Error + 'static>(
90    stage: &ExecutionStageGuard<'_>,
91    journal: &mut AttemptJournalGuard<'_>,
92    request: &IcSnapshotTransferReadRequest<'_, '_>,
93    admitted: &IcSnapshotTransferReadView<'_, '_>,
94    qualify: impl AsyncFnOnce(
95        &IcSnapshotTransferReadRequest<'_, '_>,
96        &IcSnapshotTransferReadResponse,
97    ) -> Result<MutationReceiptRequest, E>,
98) -> Result<ArtifactChecksumRecord, IcSnapshotMetadataSettlementError<E>> {
99    let receipt = qualify(request, admitted.response())
100        .await
101        .map_err(IcSnapshotMetadataSettlementError::Qualification)?;
102    let evidence = super::qualified_receipt::record_applied(
103        journal,
104        receipt,
105        request.mutation_attempt(),
106        request.payload().digest().hash(),
107        IcSnapshotMetadataSettlementError::ReceiptRequired,
108        || {
109            let digest = match admitted.reply() {
110                IcSnapshotTransferReadReply::Metadata(metadata) => metadata.digest(),
111                IcSnapshotTransferReadReply::Data(_) => {
112                    return Err(IcSnapshotMetadataSettlementError::ReceiptRequired);
113                }
114            };
115            stage.layout()?;
116            Ok(digest)
117        },
118    )?;
119    stage.layout()?;
120    Ok(evidence)
121}
122
123/// Metadata coordination failures retain exact original spending and returned evidence.
124#[derive(Debug, Error)]
125pub enum IcSnapshotMetadataExecutionError<E: std::error::Error + 'static> {
126    /// The original plan is not exactly this single metadata operation.
127    #[error("snapshot metadata requires an exact singleton original stage")]
128    OriginalMismatch,
129    /// Canonical single-read failure, including any bounded returned response.
130    #[error(transparent)]
131    Read(#[from] IcSnapshotTransferReadExecutionError<E>),
132    /// Receipt qualification failed after a response; all originals remain retained.
133    #[error("snapshot metadata reply settlement failed: {source}")]
134    AfterReply {
135        /// Exact original rejection; no retry or refund follows.
136        source: IcSnapshotMetadataSettlementError<E>,
137        /// Original bounded reply and passive claims.
138        response: Box<IcSnapshotTransferReadResponse>,
139    },
140}
141
142/// Rejection after one returned metadata reply; no second accounting owner.
143#[derive(Debug, Error)]
144pub enum IcSnapshotMetadataSettlementError<E: std::error::Error + 'static> {
145    /// Actual authentication, attribution or durable original-byte retention failed.
146    #[error("snapshot metadata qualification failed: {0}")]
147    Qualification(#[source] E),
148    /// Require an independently qualified exact original Applied receipt.
149    #[error("snapshot metadata requires an explicit original Applied receipt")]
150    ReceiptRequired,
151    /// Original stage or retained ancestor admission failed.
152    #[error(transparent)]
153    Stage(#[from] ExecutionWorkflowPersistenceError),
154    /// Original selected journal or receipt admission failed.
155    #[error(transparent)]
156    Journal(#[from] AttemptJournalError),
157}
158
159#[cfg(all(test, unix))]
160mod tests;