ic_backup/workflow/ic_snapshot_restore/mod.rs
1//! One original same-ID load/start with fresh safety and independently qualified settlement.
2
3mod source;
4pub use source::{IcRestoreSource, IcRestoreSourceError};
5
6use crate::{
7 model::{
8 attempt_journal::MutationReceiptRequest,
9 execution_workflow::ExecutionStagePredecessorRecord,
10 ic_mutation::{IcMutationAcknowledgement, IcMutationRequest, IcMutationRequestError},
11 restore_safety::{RestoreSafetyObservation, RestoreSafetyRequest},
12 },
13 ops::persistence::{
14 AttemptJournalError, ExecutionProgressPersistenceError, ExecutionStageCheckpointError,
15 ExecutionStageGuard, ExecutionWorkflowPersistenceError, RestoreSafetyPersistenceError,
16 read_restore_safety_requirement,
17 },
18 policy::{
19 ic_mutation::{IcMutationAssociationError, IcMutationReplyView, validate_acknowledgement},
20 restore_safety::{RestoreSafetyError, validate},
21 },
22 ports::ic_mutation::{IcMutationProvider, IcMutationProviderError},
23};
24use thiserror::Error;
25
26/// Execute and checkpoint one exact singleton original load or start stage.
27///
28/// Retain the complete original stage/journals, source plan, safety requirement and
29/// source references first; hold no attempt guards. `source` explicitly binds an
30/// existing snapshot or the complete original upload destination independently of
31/// the load plan. Re-admit that provenance across all boundaries. `safety` binds the exact original
32/// load/start bytes and a fresh integration-owned challenge. Read the requirement
33/// under both layouts before spending and recheck it across admission and settlement.
34/// No source is reconstructed, uploaded, replaced or released by this function.
35///
36/// After durable reservation, mandatory `admit` must qualify current authenticated
37/// control, snapshot-origin permissions, complete uploaded source, application safety,
38/// stable byte/command custody and proof of no prior dispatch. Return actual fresh
39/// safety observations; the existing policy checks exact context/inventory/selection,
40/// original lane/fence and load/start preconditions before one provider invocation.
41/// All remote preflight/qualification calls need separate prior accounting. There is
42/// no default no-external-effects lane or automatic stopped/load-success inference.
43///
44/// Mandatory `qualify` independently authenticates original successful attribution
45/// and durably retains exact request/reply evidence before its explicit Applied
46/// receipt. Record through the existing journal owner under exclusion, then release
47/// the lock and checkpoint canonical request/raw-reply evidence. Load settlement
48/// does not establish application acceptance for a later start: qualify that afresh.
49/// Pending/Applied stages never invoke callbacks/providers again. Checkpoint replay
50/// stays local; failure preserves spending, replies, obligations and references.
51/// This is one bounded step, not complete restore, terminal or fence/reference release.
52/// Admission, submission and independent qualification are awaited under the
53/// selected journal lock. Cancellation retains its current pending/Applied state
54/// and grants no repeat invocation. Qualification must durably retain returned
55/// bytes before awaiting work that could be cancelled; in-memory replies do not
56/// survive cancellation. The core imposes no executor or `Send` requirement.
57/// # Errors
58/// Rejects changed/non-singleton originals, retained source/requirement failures,
59/// spending, fresh admission/safety, provider/association, qualification/receipt or
60/// checkpoint failure. Returned bounded replies survive post-reply rejection.
61pub async fn restore_snapshot<E: std::error::Error + 'static>(
62 stage: &ExecutionStageGuard<'_>,
63 source: IcRestoreSource<'_>,
64 safety: &RestoreSafetyRequest<'_>,
65 provider: &mut impl IcMutationProvider,
66 admit: impl AsyncFnOnce(
67 &IcMutationRequest<'_>,
68 &RestoreSafetyRequest<'_>,
69 ) -> Result<RestoreSafetyObservation, E>,
70 qualify: impl AsyncFnOnce(
71 &IcMutationRequest<'_>,
72 &IcMutationAcknowledgement,
73 ) -> Result<MutationReceiptRequest, E>,
74) -> Result<(IcMutationAcknowledgement, ExecutionStagePredecessorRecord), IcRestoreExecutionError<E>>
75{
76 let plan = stage.plan();
77 let sequence = safety.binding().operation_sequence();
78 if plan.operations().len() != 1 || plan.operations()[0].operation_sequence() != sequence {
79 return Err(IcRestoreExecutionError::OriginalMismatch);
80 }
81 let retain = || {
82 source.validate(stage, safety.wire())?;
83 read_restore_safety_requirement(
84 stage.layout()?,
85 source.layout,
86 plan,
87 source.plan,
88 &safety.requirement().digest(),
89 )?;
90 Ok::<_, IcRestoreReplyError<E>>(())
91 };
92 retain()?;
93 let authority = plan
94 .attempt_authority(sequence)
95 .map_err(IcMutationRequestError::from)?;
96 let (acknowledgement, settlement) = super::ic_mutation::submit_with(
97 stage,
98 &authority,
99 safety.wire(),
100 provider,
101 async |request| {
102 let observation = admit(request, safety)
103 .await
104 .map_err(IcRestoreExecutionError::Admission)?;
105 validate(safety, &observation)?;
106 retain()?;
107 Ok::<_, IcRestoreExecutionError<E>>(())
108 },
109 async |journal, request, acknowledgement| {
110 let admitted = validate_acknowledgement(request, journal.record()?, acknowledgement)?;
111 retain()?;
112 let receipt = qualify(request, acknowledgement)
113 .await
114 .map_err(IcRestoreReplyError::Qualification)?;
115 let receipt = super::qualified_receipt::admit_applied(
116 receipt,
117 request.mutation_attempt(),
118 safety.wire().digest().hash(),
119 )
120 .ok_or(IcRestoreReplyError::ReceiptRequired)?;
121 retain()?;
122 journal.record_mutation(receipt)?;
123 retain()?;
124 let IcMutationReplyView::Lifecycle(reply) = admitted.reply() else {
125 return Err(IcRestoreReplyError::ReceiptRequired);
126 };
127 Ok::<_, IcRestoreReplyError<E>>(reply.digest())
128 },
129 )
130 .await?;
131 super::qualified_receipt::checkpoint_reply(
132 stage,
133 acknowledgement,
134 settlement,
135 |source, acknowledgement| IcRestoreExecutionError::AfterReply {
136 source,
137 acknowledgement,
138 },
139 )
140}
141
142/// Original restore-step failure; no variant authorizes reissue or cleanup.
143#[derive(Debug, Error)]
144pub enum IcRestoreExecutionError<E: std::error::Error + 'static> {
145 /// Only an exact singleton original load/start stage is admitted.
146 #[error("restore requires an exact singleton original load/start stage")]
147 OriginalMismatch,
148 /// Original workflow/stage/ancestor admission failed.
149 #[error(transparent)]
150 Stage(#[from] ExecutionWorkflowPersistenceError),
151 /// Existing journal admission failed.
152 #[error(transparent)]
153 Journal(#[from] AttemptJournalError),
154 /// Complete original progress/prerequisites or reservation failed.
155 #[error(transparent)]
156 Progress(#[from] ExecutionProgressPersistenceError),
157 /// Exact original mutation binding failed.
158 #[error(transparent)]
159 Request(#[from] IcMutationRequestError),
160 /// Original source/requirement retention failed before reply.
161 #[error(transparent)]
162 Retention(#[from] IcRestoreReplyError<E>),
163 /// Integration-owned fresh admission failed after reservation.
164 #[error("fresh restore admission failed: {0}")]
165 Admission(#[source] E),
166 /// Existing current restore-safety policy rejected before dispatch.
167 #[error(transparent)]
168 Safety(#[from] RestoreSafetyError),
169 /// Exactly one provider call failed; original spending stays pending.
170 #[error(transparent)]
171 Provider(#[from] IcMutationProviderError),
172 /// Failure after reply retains its original bounded acknowledgement.
173 #[error("restore reply settlement failed: {source}")]
174 AfterReply {
175 /// Existing evidence/qualification/persistence rejection.
176 source: IcRestoreReplyError<E>,
177 /// Exact original bounded reply without an inferred outcome.
178 acknowledgement: Box<IcMutationAcknowledgement>,
179 },
180}
181
182/// Post-reply restore rejection, preserving original receipts and pending spending.
183#[derive(Debug, Error)]
184pub enum IcRestoreReplyError<E: std::error::Error + 'static> {
185 /// Existing snapshot or complete uploaded destination no longer matches.
186 #[error(transparent)]
187 Source(#[from] IcRestoreSourceError),
188 /// Original stage/ancestor/layout admission failed.
189 #[error(transparent)]
190 Stage(#[from] ExecutionWorkflowPersistenceError),
191 /// Existing original source/requirement admission failed.
192 #[error(transparent)]
193 Retention(#[from] RestoreSafetyPersistenceError),
194 /// Existing selected journal/receipt admission failed.
195 #[error(transparent)]
196 Journal(#[from] AttemptJournalError),
197 /// Existing bounded original acknowledgement association failed.
198 #[error(transparent)]
199 Association(#[from] IcMutationAssociationError),
200 /// Authentication/attribution or durable original-byte retention failed.
201 #[error("restore reply qualification failed: {0}")]
202 Qualification(#[source] E),
203 /// Require an explicit qualified exact original Applied receipt.
204 #[error("restore requires an explicit original Applied receipt")]
205 ReceiptRequired,
206 /// Existing checkpoint failed; Applied and occupied evidence remain.
207 #[error(transparent)]
208 Checkpoint(#[from] ExecutionStageCheckpointError),
209}
210
211#[cfg(test)]
212mod tests;