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 let [operation] = plan.operations() else {
79 return Err(IcRestoreExecutionError::OriginalMismatch);
80 };
81 if operation.operation_sequence() != sequence {
82 return Err(IcRestoreExecutionError::OriginalMismatch);
83 }
84 let retain = || {
85 source.validate(stage, safety.wire())?;
86 read_restore_safety_requirement(
87 stage.layout()?,
88 source.layout,
89 plan,
90 source.plan,
91 &safety.requirement().digest(),
92 )?;
93 Ok::<_, IcRestoreReplyError<E>>(())
94 };
95 retain()?;
96 let authority = plan
97 .attempt_authority(sequence)
98 .map_err(IcMutationRequestError::from)?;
99 let (acknowledgement, settlement) = super::ic_mutation::submit_with(
100 stage,
101 &authority,
102 safety.wire(),
103 provider,
104 async |request| {
105 let observation = admit(request, safety)
106 .await
107 .map_err(IcRestoreExecutionError::Admission)?;
108 validate(safety, &observation)?;
109 retain()?;
110 Ok::<_, IcRestoreExecutionError<E>>(())
111 },
112 async |journal, request, acknowledgement| {
113 let admitted = validate_acknowledgement(request, journal.record()?, acknowledgement)?;
114 retain()?;
115 let receipt = qualify(request, acknowledgement)
116 .await
117 .map_err(IcRestoreReplyError::Qualification)?;
118 super::qualified_receipt::record_applied(
119 journal,
120 receipt,
121 request.mutation_attempt(),
122 safety.wire().digest().hash(),
123 IcRestoreReplyError::ReceiptRequired,
124 retain,
125 )?;
126 retain()?;
127 let IcMutationReplyView::Lifecycle(reply) = admitted.reply() else {
128 return Err(IcRestoreReplyError::ReceiptRequired);
129 };
130 Ok::<_, IcRestoreReplyError<E>>(reply.digest())
131 },
132 )
133 .await?;
134 super::qualified_receipt::checkpoint_reply(
135 stage,
136 acknowledgement,
137 settlement,
138 |source, acknowledgement| IcRestoreExecutionError::AfterReply {
139 source,
140 acknowledgement,
141 },
142 )
143}
144
145/// Original restore-step failure; no variant authorizes reissue or cleanup.
146#[derive(Debug, Error)]
147pub enum IcRestoreExecutionError<E: std::error::Error + 'static> {
148 /// Only an exact singleton original load/start stage is admitted.
149 #[error("restore requires an exact singleton original load/start stage")]
150 OriginalMismatch,
151 /// Original workflow/stage/ancestor admission failed.
152 #[error(transparent)]
153 Stage(#[from] ExecutionWorkflowPersistenceError),
154 /// Existing journal admission failed.
155 #[error(transparent)]
156 Journal(#[from] AttemptJournalError),
157 /// Complete original progress/prerequisites or reservation failed.
158 #[error(transparent)]
159 Progress(#[from] ExecutionProgressPersistenceError),
160 /// Exact original mutation binding failed.
161 #[error(transparent)]
162 Request(#[from] IcMutationRequestError),
163 /// Original source/requirement retention failed before reply.
164 #[error(transparent)]
165 Retention(#[from] IcRestoreReplyError<E>),
166 /// Integration-owned fresh admission failed after reservation.
167 #[error("fresh restore admission failed: {0}")]
168 Admission(#[source] E),
169 /// Existing current restore-safety policy rejected before dispatch.
170 #[error(transparent)]
171 Safety(#[from] RestoreSafetyError),
172 /// Exactly one provider call failed; original spending stays pending.
173 #[error(transparent)]
174 Provider(#[from] IcMutationProviderError),
175 /// Failure after reply retains its original bounded acknowledgement.
176 #[error("restore reply settlement failed: {source}")]
177 AfterReply {
178 /// Existing evidence/qualification/persistence rejection.
179 source: IcRestoreReplyError<E>,
180 /// Exact original bounded reply without an inferred outcome.
181 acknowledgement: Box<IcMutationAcknowledgement>,
182 },
183}
184
185/// Post-reply restore rejection, preserving original receipts and pending spending.
186#[derive(Debug, Error)]
187pub enum IcRestoreReplyError<E: std::error::Error + 'static> {
188 /// Existing snapshot or complete uploaded destination no longer matches.
189 #[error(transparent)]
190 Source(#[from] IcRestoreSourceError),
191 /// Original stage/ancestor/layout admission failed.
192 #[error(transparent)]
193 Stage(#[from] ExecutionWorkflowPersistenceError),
194 /// Existing original source/requirement admission failed.
195 #[error(transparent)]
196 Retention(#[from] RestoreSafetyPersistenceError),
197 /// Existing selected journal/receipt admission failed.
198 #[error(transparent)]
199 Journal(#[from] AttemptJournalError),
200 /// Existing bounded original acknowledgement association failed.
201 #[error(transparent)]
202 Association(#[from] IcMutationAssociationError),
203 /// Authentication/attribution or durable original-byte retention failed.
204 #[error("restore reply qualification failed: {0}")]
205 Qualification(#[source] E),
206 /// Require an explicit qualified exact original Applied receipt.
207 #[error("restore requires an explicit original Applied receipt")]
208 ReceiptRequired,
209 /// Existing checkpoint failed; Applied and occupied evidence remain.
210 #[error(transparent)]
211 Checkpoint(#[from] ExecutionStageCheckpointError),
212}
213
214#[cfg(test)]
215mod tests;