Skip to main content

ic_backup/workflow/ic_snapshot_upload/allocation/
mod.rs

1//! Original metadata allocation with independently qualified receipt and learned evidence.
2
3use super::{IcSnapshotUploadExecutionError, upload_snapshot_with};
4use crate::{
5    model::{
6        artifacts::ArtifactChecksumRecord,
7        attempt_journal::MutationReceiptRequest,
8        ic_mutation::IcMutationAcknowledgement,
9        ic_snapshot_upload::{
10            IcSnapshotUploadAttempt, IcSnapshotUploadKind, IcSnapshotUploadRequest,
11        },
12    },
13    ops::persistence::{
14        AttemptJournalError, ExecutionStageGuard, ExecutionWorkflowPersistenceError,
15    },
16    ports::ic_snapshot_upload::IcSnapshotUploadProvider,
17};
18use thiserror::Error;
19
20/// Allocate one new snapshot through an exact singleton original metadata stage.
21///
22/// Prepare the retained workflow/stage and complete original journals first; hold no
23/// attempt guards. The shared upload engine checks exact source/payload binding and
24/// durably reserves spending before mandatory fresh `admit` and one provider call. A pending or Applied
25/// allocation never dispatches again, including after failed checkpoint publication.
26///
27/// The mandatory `qualify` callback runs under the original journal lock. It must
28/// independently authenticate exclusive allocation attribution and durably retain the
29/// original request/reply bytes before returning its exact Applied receipt. Neither a
30/// decoded destination ID nor provider evidence supplies that qualification. Account
31/// any remote qualification independently before its calls; no default is installed.
32/// Admission, submission and qualification are async under their selected journal
33/// guards. Retain exact replies before cancellable qualification work. Cancellation
34/// keeps pending/Applied spending and denies allocation reentry; no runtime or `Send`
35/// bound is imposed by the core.
36///
37/// Record only that exact qualified receipt through the sole attempt owner. Return
38/// the bounded acknowledgement and canonical allocation request/raw-reply digest.
39/// The workflow owner checkpoints this learned evidence after journal exclusion
40/// ends; direct callers explicitly use the same stage owner before binding data.
41/// No data call, load/start, terminal authority or source-reference release follows.
42/// # Errors
43/// Rejects non-metadata/non-singleton originals, spending/admission/provider/reply,
44/// qualification and receipt failures. Post-reply failures retain bounded
45/// acknowledgement bytes; spending, recorded outcomes and occupied evidence remain.
46pub async fn allocate_snapshot<E: std::error::Error + 'static>(
47    stage: &ExecutionStageGuard<'_>,
48    operation_sequence: u64,
49    payload: &IcSnapshotUploadRequest<'_>,
50    provider: &mut impl IcSnapshotUploadProvider,
51    admit: impl AsyncFnOnce(&IcSnapshotUploadAttempt<'_, '_>) -> Result<(), E>,
52    qualify: impl AsyncFnOnce(
53        &IcSnapshotUploadAttempt<'_, '_>,
54        &IcMutationAcknowledgement,
55    ) -> Result<MutationReceiptRequest, E>,
56) -> Result<
57    (IcMutationAcknowledgement, ArtifactChecksumRecord),
58    IcSnapshotAllocationExecutionError<E>,
59> {
60    let operation = stage
61        .plan()
62        .singleton_operation(operation_sequence)
63        .ok_or(IcSnapshotAllocationExecutionError::OriginalMismatch)?;
64    if !matches!(payload.kind(), IcSnapshotUploadKind::Metadata)
65        || operation.request() != payload.binding_digest().hash()
66    {
67        return Err(IcSnapshotAllocationExecutionError::OriginalMismatch);
68    }
69    let (acknowledgement, settlement) = upload_snapshot_with(
70        stage,
71        operation_sequence,
72        payload,
73        provider,
74        admit,
75        async |journal, request, admitted| {
76            let receipt = qualify(request, admitted.acknowledgement())
77                .await
78                .map_err(IcSnapshotAllocationSettlementError::Qualification)?;
79            let evidence = super::super::qualified_receipt::record_applied(
80                journal,
81                receipt,
82                request.mutation_attempt(),
83                payload.binding_digest().hash(),
84                IcSnapshotAllocationSettlementError::ReceiptRequired,
85                || {
86                    stage.layout()?;
87                    Ok(admitted.reply().digest())
88                },
89            )?;
90            stage.layout()?;
91            Ok(evidence)
92        },
93    )
94    .await?;
95    super::super::qualified_receipt::retain_reply(
96        acknowledgement,
97        settlement,
98        |source, acknowledgement| IcSnapshotAllocationExecutionError::AfterReply {
99            source,
100            acknowledgement,
101        },
102    )
103}
104
105/// Allocation coordination failures retain original spending and bounded returned bytes.
106#[derive(Debug, Error)]
107pub enum IcSnapshotAllocationExecutionError<E: std::error::Error + 'static> {
108    /// Original plan is not exactly this singleton metadata allocation.
109    #[error("snapshot allocation requires an exact singleton original metadata stage")]
110    OriginalMismatch,
111    /// Canonical single-upload failure, including bounded returned acknowledgements.
112    #[error(transparent)]
113    Upload(#[from] IcSnapshotUploadExecutionError<E>),
114    /// Receipt qualification failed after reply; all originals remain retained.
115    #[error("snapshot allocation reply settlement failed: {source}")]
116    AfterReply {
117        /// Exact settlement rejection; no retry or refund follows.
118        source: IcSnapshotAllocationSettlementError<E>,
119        /// Original bounded reply and passive claims.
120        acknowledgement: Box<IcMutationAcknowledgement>,
121    },
122}
123
124/// Post-reply allocation rejection; no second accounting or outcome owner.
125#[derive(Debug, Error)]
126pub enum IcSnapshotAllocationSettlementError<E: std::error::Error + 'static> {
127    /// Independent authentication, attribution or durable original-byte retention failed.
128    #[error("snapshot allocation qualification failed: {0}")]
129    Qualification(#[source] E),
130    /// Require an independently qualified exact original Applied receipt.
131    #[error("snapshot allocation requires an explicit original Applied receipt")]
132    ReceiptRequired,
133    /// Original stage or ancestor admission failed.
134    #[error(transparent)]
135    Stage(#[from] ExecutionWorkflowPersistenceError),
136    /// Selected journal or receipt admission failed.
137    #[error(transparent)]
138    Journal(#[from] AttemptJournalError),
139}
140
141impl<E: std::error::Error + 'static> IcSnapshotAllocationExecutionError<E> {
142    /// Borrow the bounded returned evidence carried by this error, including nested failures.
143    /// Absence describes retained evidence only and grants no dispatch or retry authority.
144    #[must_use]
145    pub fn acknowledgement(&self) -> Option<&IcMutationAcknowledgement> {
146        match self {
147            Self::AfterReply {
148                acknowledgement, ..
149            } => Some(acknowledgement),
150            Self::Upload(source) => source.acknowledgement(),
151            Self::OriginalMismatch => None,
152        }
153    }
154}
155
156#[cfg(all(test, unix))]
157mod tests;