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
3mod source;
4pub use source::{IcRestoreSource, IcRestoreSourceError};
5
6use crate::ports::IcProviderError;
7use crate::{
8    model::{
9        artifacts::ArtifactChecksumRecord,
10        attempt_journal::MutationReceiptRequest,
11        ic_mutation::{IcMutationAcknowledgement, IcMutationRequest, IcMutationRequestError},
12        restore_safety::{RestoreSafetyObservation, RestoreSafetyRequest},
13    },
14    ops::persistence::{
15        AttemptJournalError, ExecutionStageGuard, ExecutionWorkflowPersistenceError,
16        RestoreSafetyPersistenceError, 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,
23};
24use thiserror::Error;
25
26/// Execute and settle 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 and return
47/// canonical request/raw-reply evidence for the workflow owner to checkpoint after
48/// releasing journal exclusion. Direct callers explicitly use the same stage owner. Load settlement
49/// does not establish application acceptance for a later start: qualify that afresh.
50/// Pending/Applied stages never invoke callbacks/providers again. Checkpoint replay
51/// stays local; failure preserves spending, replies, obligations and references.
52/// This is one bounded step, not complete restore, terminal or fence/reference release.
53/// Admission, submission and independent qualification are awaited under the
54/// selected journal lock. Cancellation retains its current pending/Applied state
55/// and grants no repeat invocation. Qualification must durably retain returned
56/// bytes before awaiting work that could be cancelled; in-memory replies do not
57/// survive cancellation. The core imposes no executor or `Send` requirement.
58/// # Errors
59/// Rejects changed/non-singleton originals, retained source/requirement failures,
60/// spending, fresh admission/safety, provider/association, qualification/receipt or
61/// returned evidence failure. Bounded replies survive post-reply rejection.
62pub async fn restore_snapshot<E: std::error::Error + 'static>(
63    stage: &ExecutionStageGuard<'_>,
64    source: IcRestoreSource<'_>,
65    safety: &RestoreSafetyRequest<'_>,
66    provider: &mut impl IcMutationProvider,
67    admit: impl AsyncFnOnce(
68        &IcMutationRequest<'_>,
69        &RestoreSafetyRequest<'_>,
70    ) -> Result<RestoreSafetyObservation, E>,
71    qualify: impl AsyncFnOnce(
72        &IcMutationRequest<'_>,
73        &IcMutationAcknowledgement,
74    ) -> Result<MutationReceiptRequest, E>,
75) -> Result<(IcMutationAcknowledgement, ArtifactChecksumRecord), IcRestoreExecutionError<E>> {
76    let plan = stage.plan();
77    let sequence = safety.binding().operation_sequence();
78    if plan.singleton_operation(sequence).is_none() {
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            super::qualified_receipt::record_applied(
116                journal,
117                receipt,
118                request.mutation_attempt(),
119                safety.wire().digest().hash(),
120                IcRestoreReplyError::ReceiptRequired,
121                retain,
122            )?;
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::retain_reply(
132        acknowledgement,
133        settlement,
134        |source, acknowledgement| IcRestoreExecutionError::AfterReply {
135            source,
136            acknowledgement,
137        },
138    )
139}
140
141/// Original restore-step failure; no variant authorizes reissue or cleanup.
142#[derive(Debug, Error)]
143pub enum IcRestoreExecutionError<E: std::error::Error + 'static> {
144    /// Shared original-stage, journal or durable reservation admission failed.
145    #[error(transparent)]
146    Reservation(#[from] super::ExecutionReservationError),
147    /// Only an exact singleton original load/start stage is admitted.
148    #[error("restore requires an exact singleton original load/start stage")]
149    OriginalMismatch,
150    /// Exact original mutation binding failed.
151    #[error(transparent)]
152    Request(#[from] IcMutationRequestError),
153    /// Original source/requirement retention failed before reply.
154    #[error(transparent)]
155    Retention(#[from] IcRestoreReplyError<E>),
156    /// Integration-owned fresh admission failed after reservation.
157    #[error("fresh restore admission failed: {0}")]
158    Admission(#[source] E),
159    /// Existing current restore-safety policy rejected before dispatch.
160    #[error(transparent)]
161    Safety(#[from] RestoreSafetyError),
162    /// Exactly one provider call failed; original spending stays pending.
163    #[error(transparent)]
164    Provider(#[from] IcProviderError),
165    /// Failure after reply retains its original bounded acknowledgement.
166    #[error("restore reply settlement failed: {source}")]
167    AfterReply {
168        /// Existing evidence/qualification/persistence rejection.
169        source: IcRestoreReplyError<E>,
170        /// Exact original bounded reply without an inferred outcome.
171        acknowledgement: Box<IcMutationAcknowledgement>,
172    },
173}
174
175/// Post-reply restore rejection, preserving original receipts and pending spending.
176#[derive(Debug, Error)]
177pub enum IcRestoreReplyError<E: std::error::Error + 'static> {
178    /// Existing snapshot or complete uploaded destination no longer matches.
179    #[error(transparent)]
180    Source(#[from] IcRestoreSourceError),
181    /// Original stage/ancestor/layout admission failed.
182    #[error(transparent)]
183    Stage(#[from] ExecutionWorkflowPersistenceError),
184    /// Existing original source/requirement admission failed.
185    #[error(transparent)]
186    Retention(#[from] RestoreSafetyPersistenceError),
187    /// Existing selected journal/receipt admission failed.
188    #[error(transparent)]
189    Journal(#[from] AttemptJournalError),
190    /// Existing bounded original acknowledgement association failed.
191    #[error(transparent)]
192    Association(#[from] IcMutationAssociationError),
193    /// Authentication/attribution or durable original-byte retention failed.
194    #[error("restore reply qualification failed: {0}")]
195    Qualification(#[source] E),
196    /// Require an explicit qualified exact original Applied receipt.
197    #[error("restore requires an explicit original Applied receipt")]
198    ReceiptRequired,
199}
200
201#[cfg(test)]
202mod tests;