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