Skip to main content

ic_backup/workflow/ic_snapshot_upload/allocation/
mod.rs

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