ic_backup/workflow/ic_snapshot_capture/mod.rs
1//! One freshly reserved capture update under exact original stage evidence.
2
3use crate::policy::ic_mutation::IcMutationReplyView;
4use crate::ports::IcProviderError;
5use crate::{
6 model::{
7 artifacts::ArtifactChecksumRecord,
8 attempt_journal::MutationReceiptRequest,
9 ic_mutation::{IcMutationAcknowledgement, IcMutationRequest, IcMutationRequestError},
10 ic_request::{IcManagementMethodRecord, IcManagementRequestRecord},
11 operation_plan::OperationPlanError,
12 },
13 ops::persistence::{
14 AttemptJournalError, ExecutionStageGuard, ExecutionWorkflowPersistenceError,
15 },
16 policy::ic_mutation::{IcMutationAssociationError, validate_acknowledgement},
17 ports::ic_mutation::IcMutationProvider,
18};
19use thiserror::Error;
20
21/// Durably reserve an original capture, freshly admit it and submit one exact update.
22///
23/// Hold no attempt guards on entry. Only `TakeCanisterSnapshot` is accepted, with
24/// canonical original target/wire binding checked before spending. The existing
25/// journal is opened, never created, and complete original-plan admission owns its
26/// durable reservation. Keep the selected journal locked through fresh admission,
27/// one provider invocation and bounded passive acknowledgement association.
28/// Retained stage/ancestor checks bracket dispatch. Pending or Applied attempts
29/// cannot invoke admission or dispatch again; reopening grants no recapture.
30///
31/// The mandatory fallible `admit` callback must qualify fresh actual context/control,
32/// original capture consistency, quiescence/fence obligations and exclusive
33/// never-dispatched command custody. Any remote preflight calls need their own prior
34/// accounting. There is no default permission, provider or application consistency
35/// lane. The existing provider retains its authenticated single-update contract.
36/// Admission and submission are awaited while the selected journal stays locked.
37/// Cancellation releases that lock while retaining pending or recorded Applied spending;
38/// even cancellation before dispatch grants no repeat callback or submission.
39///
40/// `qualify` must independently authenticate original attribution and durably retain
41/// exact request/reply bytes before returning its explicit Applied receipt. It runs
42/// under the same journal lock; retain bytes before any cancellable await. Remote
43/// qualification requires separate prior accounting. Only this exact qualified
44/// receipt is recorded. Success returns the acknowledgement and canonical capture
45/// reply digest for the workflow owner to checkpoint after journal exclusion ends.
46/// An ID or matching wire reply alone supplies no outcome or release authority.
47/// # Errors
48/// Rejects noncapture/changed payloads, original evidence, spending, fresh admission,
49/// provider/association, qualification or receipt failure. Typed post-reply errors
50/// retain the bounded acknowledgement. No failure permits retry or refund.
51pub async fn capture_snapshot<E: std::error::Error + 'static>(
52 stage: &ExecutionStageGuard<'_>,
53 operation_sequence: u64,
54 payload: &IcManagementRequestRecord,
55 provider: &mut impl IcMutationProvider,
56 admit: impl AsyncFnOnce(&IcMutationRequest<'_>) -> Result<(), E>,
57 qualify: impl AsyncFnOnce(
58 &IcMutationRequest<'_>,
59 &IcMutationAcknowledgement,
60 ) -> Result<MutationReceiptRequest, E>,
61) -> Result<(IcMutationAcknowledgement, ArtifactChecksumRecord), IcSnapshotCaptureExecutionError<E>>
62{
63 if payload.method() != IcManagementMethodRecord::TakeCanisterSnapshot {
64 return Err(IcSnapshotCaptureExecutionError::CaptureRequired);
65 }
66 let plan = stage.plan();
67 let authority = plan.attempt_authority(operation_sequence)?;
68 let (acknowledgement, admission) = super::ic_mutation::submit_with(
69 stage,
70 &authority,
71 payload,
72 provider,
73 async |request| {
74 admit(request)
75 .await
76 .map_err(IcSnapshotCaptureExecutionError::Admission)?;
77 stage
78 .layout()
79 .map_err(super::ExecutionReservationError::from)?;
80 Ok::<_, IcSnapshotCaptureExecutionError<E>>(())
81 },
82 async |journal, request, acknowledgement| {
83 let admitted = validate_acknowledgement(request, journal.record()?, acknowledgement)?;
84 stage.layout()?;
85 let receipt = qualify(request, acknowledgement)
86 .await
87 .map_err(IcSnapshotCaptureReplyError::Qualification)?;
88 super::qualified_receipt::record_applied(
89 journal,
90 receipt,
91 request.mutation_attempt(),
92 payload.digest().hash(),
93 IcSnapshotCaptureReplyError::ReceiptRequired,
94 || {
95 stage.layout()?;
96 Ok(())
97 },
98 )?;
99 stage.layout()?;
100 let IcMutationReplyView::Capture(reply) = admitted.reply() else {
101 return Err(IcSnapshotCaptureReplyError::ReceiptRequired);
102 };
103 Ok::<_, IcSnapshotCaptureReplyError<E>>(reply.digest())
104 },
105 )
106 .await?;
107 let admitted = admission;
108 super::qualified_receipt::retain_reply(acknowledgement, admitted, |source, acknowledgement| {
109 IcSnapshotCaptureExecutionError::AfterReply {
110 source,
111 acknowledgement,
112 }
113 })
114}
115
116/// Capture-step failures retain original spending and any bounded returned acknowledgement.
117#[derive(Debug, Error)]
118pub enum IcSnapshotCaptureExecutionError<E: std::error::Error + 'static> {
119 /// Shared original-stage, journal or durable reservation admission failed.
120 #[error(transparent)]
121 Reservation(#[from] super::ExecutionReservationError),
122 /// Only the original capture method may enter this coordinator.
123 #[error("snapshot capture requires take_canister_snapshot")]
124 CaptureRequired,
125 /// Original operation authority cannot be derived.
126 #[error(transparent)]
127 Plan(#[from] OperationPlanError),
128 /// Canonical original mutation binding or current reservation differs.
129 #[error(transparent)]
130 Request(#[from] IcMutationRequestError),
131 /// Fresh integration-owned admission rejected after reservation.
132 #[error("fresh snapshot capture admission failed: {0}")]
133 Admission(#[source] E),
134 /// The single provider call failed; spending remains pending.
135 #[error(transparent)]
136 Provider(#[from] IcProviderError),
137 /// Admission after a reply failed, retaining its complete bounded evidence.
138 #[error("snapshot capture reply rejected: {source}")]
139 AfterReply {
140 /// Typed original admission failure.
141 source: IcSnapshotCaptureReplyError<E>,
142 /// Complete returned passive evidence, without settlement authority.
143 acknowledgement: Box<IcMutationAcknowledgement>,
144 },
145}
146
147/// Admission failure after a returned snapshot capture reply.
148#[derive(Debug, Error)]
149pub enum IcSnapshotCaptureReplyError<E: std::error::Error + 'static> {
150 /// Independent attribution or durable reply retention failed.
151 #[error("snapshot capture qualification failed: {0}")]
152 Qualification(#[source] E),
153 /// Explicit receipt differs from this original Applied capture.
154 #[error("snapshot capture requires an explicit original Applied receipt")]
155 ReceiptRequired,
156 /// Original selected journal cannot be re-admitted.
157 #[error(transparent)]
158 Journal(#[from] AttemptJournalError),
159 /// Passive exact reply association failed.
160 #[error(transparent)]
161 Association(#[from] IcMutationAssociationError),
162 /// Original stage or ancestor evidence cannot be re-admitted.
163 #[error(transparent)]
164 Stage(#[from] ExecutionWorkflowPersistenceError),
165}
166
167#[cfg(all(test, unix))]
168mod tests;