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 [operation] = stage.plan().operations() else {
64 return Err(IcSnapshotAllocationExecutionError::OriginalMismatch);
65 };
66 if !matches!(payload.kind(), IcSnapshotUploadKind::Metadata)
67 || operation.operation_sequence() != operation_sequence
68 || operation.request() != payload.binding_digest().hash()
69 {
70 return Err(IcSnapshotAllocationExecutionError::OriginalMismatch);
71 }
72 let (acknowledgement, settlement) = upload_snapshot_with(
73 stage,
74 operation_sequence,
75 payload,
76 provider,
77 admit,
78 async |journal, request, admitted| {
79 let receipt = qualify(request, admitted.acknowledgement())
80 .await
81 .map_err(IcSnapshotAllocationSettlementError::Qualification)?;
82 super::super::qualified_receipt::record_applied(
83 journal,
84 receipt,
85 request.mutation_attempt(),
86 payload.binding_digest().hash(),
87 IcSnapshotAllocationSettlementError::ReceiptRequired,
88 || {
89 stage.layout()?;
90 Ok(admitted.reply().digest())
91 },
92 )
93 },
94 )
95 .await?;
96 // The upload owner releases its journal before checkpoint admission locks it again.
97 super::super::qualified_receipt::checkpoint_reply(
98 stage,
99 acknowledgement,
100 settlement,
101 |source, acknowledgement| IcSnapshotAllocationExecutionError::AfterReply {
102 source,
103 acknowledgement,
104 },
105 )
106}
107
108/// Allocation coordination failures retain original spending and bounded returned bytes.
109#[derive(Debug, Error)]
110pub enum IcSnapshotAllocationExecutionError<E: std::error::Error + 'static> {
111 /// Original plan is not exactly this singleton metadata allocation.
112 #[error("snapshot allocation requires an exact singleton original metadata stage")]
113 OriginalMismatch,
114 /// Canonical single-upload failure, including bounded returned acknowledgements.
115 #[error(transparent)]
116 Upload(#[from] IcSnapshotUploadExecutionError<E>),
117 /// Receipt or checkpoint failed after reply; all originals remain retained.
118 #[error("snapshot allocation reply settlement failed: {source}")]
119 AfterReply {
120 /// Exact settlement rejection; no retry or refund follows.
121 source: IcSnapshotAllocationSettlementError<E>,
122 /// Original bounded reply and passive claims.
123 acknowledgement: Box<IcMutationAcknowledgement>,
124 },
125}
126
127/// Post-reply allocation rejection; no second accounting or outcome owner.
128#[derive(Debug, Error)]
129pub enum IcSnapshotAllocationSettlementError<E: std::error::Error + 'static> {
130 /// Independent authentication, attribution or durable original-byte retention failed.
131 #[error("snapshot allocation qualification failed: {0}")]
132 Qualification(#[source] E),
133 /// Require an independently qualified exact original Applied receipt.
134 #[error("snapshot allocation requires an explicit original Applied receipt")]
135 ReceiptRequired,
136 /// Original stage or ancestor admission failed.
137 #[error(transparent)]
138 Stage(#[from] ExecutionWorkflowPersistenceError),
139 /// Selected journal or receipt admission failed.
140 #[error(transparent)]
141 Journal(#[from] AttemptJournalError),
142 /// Canonical all-Applied checkpoint publication/re-admission failed.
143 #[error(transparent)]
144 Checkpoint(#[from] ExecutionStageCheckpointError),
145}
146
147#[cfg(all(test, unix))]
148mod tests;