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