Skip to main content

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;