Skip to main content

ic_backup/workflow/ic_snapshot_restore/
mod.rs

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