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///
34/// Success returns a bounded passive acknowledgement and leaves spending pending.
35/// Integrations authenticate exact original attribution and explicitly record any
36/// qualified receipt using the existing journal owner. An ID or matching wire reply
37/// supplies no automatic outcome, complete capture/download, terminal or fence release.
38/// # Errors
39/// Rejects noncapture/changed payloads, original evidence, spending, fresh admission,
40/// provider failures or malformed/mismatched replies. All post-reservation failures
41/// retain consumption; returned acknowledgements survive later rejection in typed
42/// errors. No failure permits retry, recapture, refund, cleanup or automatic uncertainty.
43pub fn capture_snapshot<E: std::error::Error + 'static>(
44    stage: &ExecutionStageGuard<'_>,
45    operation_sequence: u64,
46    payload: &IcManagementRequestRecord,
47    provider: &mut impl IcMutationProvider,
48    admit: impl FnOnce(&IcMutationRequest<'_>) -> Result<(), E>,
49) -> Result<IcMutationAcknowledgement, IcSnapshotCaptureExecutionError<E>> {
50    if payload.method() != IcManagementMethodRecord::TakeCanisterSnapshot {
51        return Err(IcSnapshotCaptureExecutionError::CaptureRequired);
52    }
53    let plan = stage.plan();
54    let authority = plan.attempt_authority(operation_sequence)?;
55    payload
56        .validate_mutation_binding(authority.binding())
57        .map_err(IcMutationRequestError::from)?;
58    let mut journal = AttemptJournalGuard::open(stage.layout()?, &authority)?;
59    journal.reserve_planned_mutation(&plan.digest())?;
60    let request = IcMutationRequest::new(plan, operation_sequence, journal.record()?, payload)?;
61    admit(&request).map_err(IcSnapshotCaptureExecutionError::Admission)?;
62    stage.layout()?;
63    let acknowledgement = provider.submit_mutation(&request)?;
64    let association = match journal.record() {
65        Ok(record) => record,
66        Err(source) => {
67            return Err(IcSnapshotCaptureExecutionError::AfterReplyJournal {
68                source,
69                acknowledgement: Box::new(acknowledgement),
70            });
71        }
72    };
73    if let Err(source) = validate_acknowledgement(&request, association, &acknowledgement) {
74        return Err(IcSnapshotCaptureExecutionError::Association {
75            source,
76            acknowledgement: Box::new(acknowledgement),
77        });
78    }
79    if let Err(source) = stage.layout() {
80        return Err(IcSnapshotCaptureExecutionError::AfterReplyStage {
81            source,
82            acknowledgement: Box::new(acknowledgement),
83        });
84    }
85    Ok(acknowledgement)
86}
87
88/// Capture-step failures retain original spending and any bounded returned acknowledgement.
89#[derive(Debug, Error)]
90pub enum IcSnapshotCaptureExecutionError<E: std::error::Error + 'static> {
91    /// Only the original capture method may enter this coordinator.
92    #[error("snapshot capture requires take_canister_snapshot")]
93    CaptureRequired,
94    /// Original operation authority cannot be derived.
95    #[error(transparent)]
96    Plan(#[from] OperationPlanError),
97    /// Workflow, stage or original ancestor evidence failed admission.
98    #[error(transparent)]
99    Stage(#[from] ExecutionWorkflowPersistenceError),
100    /// Original selected journal admission failed.
101    #[error(transparent)]
102    Journal(#[from] AttemptJournalError),
103    /// Complete original progress, prerequisites or durable reservation failed.
104    #[error(transparent)]
105    Progress(#[from] ExecutionProgressPersistenceError),
106    /// Canonical original mutation binding or current reservation differs.
107    #[error(transparent)]
108    Request(#[from] IcMutationRequestError),
109    /// Fresh integration-owned admission rejected after reservation.
110    #[error("fresh snapshot capture admission failed: {0}")]
111    Admission(#[source] E),
112    /// The single provider call failed; spending remains pending.
113    #[error(transparent)]
114    Provider(#[from] IcMutationProviderError),
115    /// Passive bounded association rejected a returned acknowledgement.
116    #[error("snapshot capture acknowledgement association failed: {source}")]
117    Association {
118        /// Existing canonical mutation association rejection.
119        source: IcMutationAssociationError,
120        /// Exact returned acknowledgement, without authenticated outcome.
121        acknowledgement: Box<IcMutationAcknowledgement>,
122    },
123    /// Selected journal re-admission failed after a reply.
124    #[error("snapshot capture journal changed after reply: {source}")]
125    AfterReplyJournal {
126        /// Original journal rejection.
127        source: AttemptJournalError,
128        /// Exact bounded returned acknowledgement.
129        acknowledgement: Box<IcMutationAcknowledgement>,
130    },
131    /// Stage or ancestor re-admission failed after a reply.
132    #[error("snapshot capture stage changed after reply: {source}")]
133    AfterReplyStage {
134        /// Original stage or ancestor rejection.
135        source: ExecutionWorkflowPersistenceError,
136        /// Exact bounded returned acknowledgement.
137        acknowledgement: Box<IcMutationAcknowledgement>,
138    },
139}
140
141#[cfg(all(test, unix))]
142mod tests;