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/// # Errors
50/// Rejects changed/non-singleton originals, retained source/requirement failures,
51/// spending, fresh admission/safety, provider/association, qualification/receipt or
52/// checkpoint failure. Returned bounded replies survive post-reply rejection.
53pub fn restore_snapshot<E: std::error::Error + 'static>(
54    stage: &ExecutionStageGuard<'_>,
55    source_layout: &BackupLayoutGuard,
56    source_plan: &OperationPlanRecord,
57    safety: &RestoreSafetyRequest<'_>,
58    provider: &mut impl IcMutationProvider,
59    admit: impl FnOnce(
60        &IcMutationRequest<'_>,
61        &RestoreSafetyRequest<'_>,
62    ) -> Result<RestoreSafetyObservation, E>,
63    qualify: impl FnOnce(
64        &IcMutationRequest<'_>,
65        &IcMutationAcknowledgement,
66    ) -> Result<MutationReceiptRequest, E>,
67) -> Result<(IcMutationAcknowledgement, ExecutionStagePredecessorRecord), IcRestoreExecutionError<E>>
68{
69    let plan = stage.plan();
70    let sequence = safety.binding().operation_sequence();
71    if plan.operations().len() != 1 || plan.operations()[0].operation_sequence() != sequence {
72        return Err(IcRestoreExecutionError::OriginalMismatch);
73    }
74    let retain = || {
75        read_restore_safety_requirement(
76            stage.layout()?,
77            source_layout,
78            plan,
79            source_plan,
80            &safety.requirement().digest(),
81        )?;
82        Ok::<_, IcRestoreReplyError<E>>(())
83    };
84    retain()?;
85    let authority = plan
86        .attempt_authority(sequence)
87        .map_err(IcMutationRequestError::from)?;
88    safety
89        .wire()
90        .validate_mutation_binding(authority.binding())
91        .map_err(IcMutationRequestError::from)?;
92    let mut journal = AttemptJournalGuard::open(stage.layout()?, &authority)?;
93    journal.reserve_planned_mutation(&plan.digest())?;
94    let request = IcMutationRequest::new(plan, sequence, journal.record()?, safety.wire())?;
95    let observation = admit(&request, safety).map_err(IcRestoreExecutionError::Admission)?;
96    validate(safety, &observation)?;
97    retain()?;
98    let acknowledgement = provider.submit_mutation(&request)?;
99    let settle = || {
100        let admitted = validate_acknowledgement(&request, journal.record()?, &acknowledgement)?;
101        retain()?;
102        let receipt =
103            qualify(&request, &acknowledgement).map_err(IcRestoreReplyError::Qualification)?;
104        if receipt.outcome != MutationOutcomeRecord::Applied
105            || receipt.attempt != request.mutation_attempt()
106            || receipt.request != safety.wire().digest().hash()
107        {
108            return Err(IcRestoreReplyError::ReceiptRequired);
109        }
110        retain()?;
111        journal.record_mutation(receipt)?;
112        retain()?;
113        let IcMutationReplyView::Lifecycle(reply) = admitted.reply() else {
114            return Err(IcRestoreReplyError::ReceiptRequired);
115        };
116        Ok(reply.digest())
117    };
118    let evidence = match settle() {
119        Ok(evidence) => evidence,
120        Err(source) => {
121            return Err(IcRestoreExecutionError::AfterReply {
122                source,
123                acknowledgement: Box::new(acknowledgement),
124            });
125        }
126    };
127    drop(journal);
128    match stage.checkpoint(evidence) {
129        Ok(predecessor) => Ok((acknowledgement, predecessor)),
130        Err(source) => Err(IcRestoreExecutionError::AfterReply {
131            source: IcRestoreReplyError::Checkpoint(source),
132            acknowledgement: Box::new(acknowledgement),
133        }),
134    }
135}
136
137/// Original restore-step failure; no variant authorizes reissue or cleanup.
138#[derive(Debug, Error)]
139pub enum IcRestoreExecutionError<E: std::error::Error + 'static> {
140    /// Only an exact singleton original load/start stage is admitted.
141    #[error("restore requires an exact singleton original load/start stage")]
142    OriginalMismatch,
143    /// Original workflow/stage/ancestor admission failed.
144    #[error(transparent)]
145    Stage(#[from] ExecutionWorkflowPersistenceError),
146    /// Existing journal admission failed.
147    #[error(transparent)]
148    Journal(#[from] AttemptJournalError),
149    /// Complete original progress/prerequisites or reservation failed.
150    #[error(transparent)]
151    Progress(#[from] ExecutionProgressPersistenceError),
152    /// Exact original mutation binding failed.
153    #[error(transparent)]
154    Request(#[from] IcMutationRequestError),
155    /// Original source/requirement retention failed before reply.
156    #[error(transparent)]
157    Retention(#[from] IcRestoreReplyError<E>),
158    /// Integration-owned fresh admission failed after reservation.
159    #[error("fresh restore admission failed: {0}")]
160    Admission(#[source] E),
161    /// Existing current restore-safety policy rejected before dispatch.
162    #[error(transparent)]
163    Safety(#[from] RestoreSafetyError),
164    /// Exactly one provider call failed; original spending stays pending.
165    #[error(transparent)]
166    Provider(#[from] IcMutationProviderError),
167    /// Failure after reply retains its original bounded acknowledgement.
168    #[error("restore reply settlement failed: {source}")]
169    AfterReply {
170        /// Existing evidence/qualification/persistence rejection.
171        source: IcRestoreReplyError<E>,
172        /// Exact original bounded reply without an inferred outcome.
173        acknowledgement: Box<IcMutationAcknowledgement>,
174    },
175}
176
177/// Post-reply restore rejection, preserving original receipts and pending spending.
178#[derive(Debug, Error)]
179pub enum IcRestoreReplyError<E: std::error::Error + 'static> {
180    /// Original stage/ancestor/layout admission failed.
181    #[error(transparent)]
182    Stage(#[from] ExecutionWorkflowPersistenceError),
183    /// Existing original source/requirement admission failed.
184    #[error(transparent)]
185    Retention(#[from] RestoreSafetyPersistenceError),
186    /// Existing selected journal/receipt admission failed.
187    #[error(transparent)]
188    Journal(#[from] AttemptJournalError),
189    /// Existing bounded original acknowledgement association failed.
190    #[error(transparent)]
191    Association(#[from] IcMutationAssociationError),
192    /// Authentication/attribution or durable original-byte retention failed.
193    #[error("restore reply qualification failed: {0}")]
194    Qualification(#[source] E),
195    /// Require an explicit qualified exact original Applied receipt.
196    #[error("restore requires an explicit original Applied receipt")]
197    ReceiptRequired,
198    /// Existing checkpoint failed; Applied and occupied evidence remain.
199    #[error(transparent)]
200    Checkpoint(#[from] ExecutionStageCheckpointError),
201}
202
203#[cfg(test)]
204mod tests;