Skip to main content

ic_backup/workflow/ic_snapshot_metadata/
mod.rs

1//! One original metadata stage, with explicit qualified receipt and learned checkpoint.
2
3use crate::{
4    model::{
5        attempt_journal::{MutationOutcomeRecord, MutationReceiptRequest},
6        execution_workflow::ExecutionStagePredecessorRecord,
7        ic_snapshot_metadata::IcSnapshotMetadataRequest,
8        ic_snapshot_transfer_read::{
9            IcSnapshotTransferReadError, IcSnapshotTransferReadPayload,
10            IcSnapshotTransferReadRequest, IcSnapshotTransferReadResponse,
11        },
12    },
13    ops::persistence::{
14        AttemptJournalError, AttemptJournalGuard, ExecutionStageCheckpointError,
15        ExecutionStageGuard, ExecutionWorkflowPersistenceError,
16    },
17    policy::ic_snapshot_transfer_read::{
18        IcSnapshotTransferReadAssociationError, IcSnapshotTransferReadReply, validate_response,
19    },
20    ports::ic_snapshot_transfer_read::IcSnapshotTransferReadProvider,
21    workflow::ic_snapshot_transfer_read::{IcSnapshotTransferReadExecutionError, read_snapshot},
22};
23use thiserror::Error;
24
25/// Read one new exact singleton metadata stage, record its qualified receipt and checkpoint.
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///
39/// Record the receipt through the sole spending owner, release its lock, then publish
40/// the canonical all-Applied checkpoint with the exact request/raw-metadata digest as
41/// learned evidence. The returned response and predecessor can feed the existing
42/// metadata decoder and `IcSnapshotDownloadPlan::bind`; preparing the data stage and
43/// writer remains explicit. Failure retains spending, receipts, checkpoints and bytes;
44/// post-read errors retain the bounded response. Reopen/checkpoint recovery never
45/// repeats a read. No data calls, artifact, default Agent bridge, complete backup,
46/// terminal proof or fence/reference release follows.
47/// # Errors
48/// Rejects non-singleton or changed originals, spending, fresh admission, provider,
49/// association, qualification, receipt and checkpoint failures without cleanup/refund.
50pub fn read_snapshot_metadata<E: std::error::Error + 'static>(
51    stage: &ExecutionStageGuard<'_>,
52    operation_sequence: u64,
53    payload: &IcSnapshotMetadataRequest,
54    provider: &mut impl IcSnapshotTransferReadProvider,
55    admit: impl FnOnce(&IcSnapshotTransferReadRequest<'_, '_>) -> Result<(), E>,
56    qualify: impl FnOnce(
57        &IcSnapshotTransferReadRequest<'_, '_>,
58        &IcSnapshotTransferReadResponse,
59    ) -> Result<MutationReceiptRequest, E>,
60) -> Result<
61    (
62        IcSnapshotTransferReadResponse,
63        ExecutionStagePredecessorRecord,
64    ),
65    IcSnapshotMetadataExecutionError<E>,
66> {
67    let operations = stage.plan().operations();
68    if operations.len() != 1
69        || operations[0].operation_sequence() != operation_sequence
70        || operations[0].request() != payload.digest().hash()
71    {
72        return Err(IcSnapshotMetadataExecutionError::OriginalMismatch);
73    }
74    let response = read_snapshot(
75        stage,
76        operation_sequence,
77        IcSnapshotTransferReadPayload::Metadata(payload),
78        provider,
79        admit,
80    )?;
81    match settle_metadata(stage, operation_sequence, payload, &response, qualify) {
82        Ok(predecessor) => Ok((response, predecessor)),
83        Err(source) => Err(IcSnapshotMetadataExecutionError::AfterReply {
84            source,
85            response: Box::new(response),
86        }),
87    }
88}
89
90fn settle_metadata<E: std::error::Error + 'static>(
91    stage: &ExecutionStageGuard<'_>,
92    sequence: u64,
93    payload: &IcSnapshotMetadataRequest,
94    response: &IcSnapshotTransferReadResponse,
95    qualify: impl FnOnce(
96        &IcSnapshotTransferReadRequest<'_, '_>,
97        &IcSnapshotTransferReadResponse,
98    ) -> Result<MutationReceiptRequest, E>,
99) -> Result<ExecutionStagePredecessorRecord, IcSnapshotMetadataSettlementError<E>> {
100    let plan = stage.plan();
101    let authority = plan
102        .attempt_authority(sequence)
103        .map_err(IcSnapshotTransferReadError::from)?;
104    let mut journal = AttemptJournalGuard::open(stage.layout()?, &authority)?;
105    let request = IcSnapshotTransferReadRequest::new(
106        plan,
107        sequence,
108        journal.record()?,
109        IcSnapshotTransferReadPayload::Metadata(payload),
110    )?;
111    let admitted = validate_response(&request, journal.record()?, response)?;
112    let receipt =
113        qualify(&request, response).map_err(IcSnapshotMetadataSettlementError::Qualification)?;
114    if receipt.outcome != MutationOutcomeRecord::Applied
115        || receipt.attempt != request.mutation_attempt()
116        || receipt.request != payload.digest().hash()
117    {
118        return Err(IcSnapshotMetadataSettlementError::ReceiptRequired);
119    }
120    let IcSnapshotTransferReadReply::Metadata(metadata) = admitted.reply() else {
121        return Err(IcSnapshotMetadataSettlementError::ReceiptRequired);
122    };
123    stage.layout()?;
124    journal.record_mutation(receipt)?;
125    drop(journal);
126    Ok(stage.checkpoint(metadata.digest())?)
127}
128
129/// Metadata coordination failures retain exact original spending and returned evidence.
130#[derive(Debug, Error)]
131pub enum IcSnapshotMetadataExecutionError<E: std::error::Error + 'static> {
132    /// The original plan is not exactly this single metadata operation.
133    #[error("snapshot metadata requires an exact singleton original stage")]
134    OriginalMismatch,
135    /// Canonical single-read failure, including any bounded returned response.
136    #[error(transparent)]
137    Read(#[from] IcSnapshotTransferReadExecutionError<E>),
138    /// Receipt or checkpoint failed after a response; all originals remain retained.
139    #[error("snapshot metadata reply settlement failed: {source}")]
140    AfterReply {
141        /// Exact original rejection; no retry or refund follows.
142        source: IcSnapshotMetadataSettlementError<E>,
143        /// Original bounded reply and passive claims.
144        response: Box<IcSnapshotTransferReadResponse>,
145    },
146}
147
148/// Rejection after one returned metadata reply; no second accounting owner.
149#[derive(Debug, Error)]
150pub enum IcSnapshotMetadataSettlementError<E: std::error::Error + 'static> {
151    /// Actual authentication, attribution or durable original-byte retention failed.
152    #[error("snapshot metadata qualification failed: {0}")]
153    Qualification(#[source] E),
154    /// Require an independently qualified exact original Applied receipt.
155    #[error("snapshot metadata requires an explicit original Applied receipt")]
156    ReceiptRequired,
157    /// Original stage or retained ancestor admission failed.
158    #[error(transparent)]
159    Stage(#[from] ExecutionWorkflowPersistenceError),
160    /// Original selected journal or receipt admission failed.
161    #[error(transparent)]
162    Journal(#[from] AttemptJournalError),
163    /// Exact original request/current reservation admission failed.
164    #[error(transparent)]
165    Request(#[from] IcSnapshotTransferReadError),
166    /// Existing bounded passive response admission failed.
167    #[error(transparent)]
168    Association(#[from] IcSnapshotTransferReadAssociationError),
169    /// Canonical all-Applied checkpoint publication/re-admission failed.
170    #[error(transparent)]
171    Checkpoint(#[from] ExecutionStageCheckpointError),
172}
173
174#[cfg(all(test, unix))]
175mod tests;