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 admit(&request)
65 .await
66 .map_err(IcSnapshotCaptureExecutionError::Admission)?;
67 stage.layout()?;
68 let acknowledgement = provider
69 .submit_mutation(&request, journal.record()?)
70 .await?;
71 let association = match journal.record() {
72 Ok(record) => record,
73 Err(source) => {
74 return Err(IcSnapshotCaptureExecutionError::AfterReplyJournal {
75 source,
76 acknowledgement: Box::new(acknowledgement),
77 });
78 }
79 };
80 if let Err(source) = validate_acknowledgement(&request, association, &acknowledgement) {
81 return Err(IcSnapshotCaptureExecutionError::Association {
82 source,
83 acknowledgement: Box::new(acknowledgement),
84 });
85 }
86 if let Err(source) = stage.layout() {
87 return Err(IcSnapshotCaptureExecutionError::AfterReplyStage {
88 source,
89 acknowledgement: Box::new(acknowledgement),
90 });
91 }
92 Ok(acknowledgement)
93}
94
95/// Capture-step failures retain original spending and any bounded returned acknowledgement.
96#[derive(Debug, Error)]
97pub enum IcSnapshotCaptureExecutionError<E: std::error::Error + 'static> {
98 /// Only the original capture method may enter this coordinator.
99 #[error("snapshot capture requires take_canister_snapshot")]
100 CaptureRequired,
101 /// Original operation authority cannot be derived.
102 #[error(transparent)]
103 Plan(#[from] OperationPlanError),
104 /// Workflow, stage or original ancestor evidence failed admission.
105 #[error(transparent)]
106 Stage(#[from] ExecutionWorkflowPersistenceError),
107 /// Original selected journal admission failed.
108 #[error(transparent)]
109 Journal(#[from] AttemptJournalError),
110 /// Complete original progress, prerequisites or durable reservation failed.
111 #[error(transparent)]
112 Progress(#[from] ExecutionProgressPersistenceError),
113 /// Canonical original mutation binding or current reservation differs.
114 #[error(transparent)]
115 Request(#[from] IcMutationRequestError),
116 /// Fresh integration-owned admission rejected after reservation.
117 #[error("fresh snapshot capture admission failed: {0}")]
118 Admission(#[source] E),
119 /// The single provider call failed; spending remains pending.
120 #[error(transparent)]
121 Provider(#[from] IcMutationProviderError),
122 /// Passive bounded association rejected a returned acknowledgement.
123 #[error("snapshot capture acknowledgement association failed: {source}")]
124 Association {
125 /// Existing canonical mutation association rejection.
126 source: IcMutationAssociationError,
127 /// Exact returned acknowledgement, without authenticated outcome.
128 acknowledgement: Box<IcMutationAcknowledgement>,
129 },
130 /// Selected journal re-admission failed after a reply.
131 #[error("snapshot capture journal changed after reply: {source}")]
132 AfterReplyJournal {
133 /// Original journal rejection.
134 source: AttemptJournalError,
135 /// Exact bounded returned acknowledgement.
136 acknowledgement: Box<IcMutationAcknowledgement>,
137 },
138 /// Stage or ancestor re-admission failed after a reply.
139 #[error("snapshot capture stage changed after reply: {source}")]
140 AfterReplyStage {
141 /// Original stage or ancestor rejection.
142 source: ExecutionWorkflowPersistenceError,
143 /// Exact bounded returned acknowledgement.
144 acknowledgement: Box<IcMutationAcknowledgement>,
145 },
146}
147
148#[cfg(all(test, unix))]
149mod tests;