Skip to main content

ic_backup/workflow/ic_snapshot_transfer_read/
mod.rs

1//! One freshly reserved metadata/data update; no automatic receipt or retry.
2
3use crate::{
4    model::{
5        ic_snapshot_transfer_read::{
6            IcSnapshotTransferReadError, IcSnapshotTransferReadPayload,
7            IcSnapshotTransferReadRequest, IcSnapshotTransferReadResponse,
8        },
9        operation_plan::OperationPlanError,
10    },
11    ops::persistence::{
12        AttemptJournalError, AttemptJournalGuard, ExecutionProgressPersistenceError,
13        ExecutionStageGuard, ExecutionWorkflowPersistenceError,
14    },
15    policy::ic_snapshot_transfer_read::{
16        IcSnapshotTransferReadAssociationError, validate_response,
17    },
18    ports::{
19        ic_observation::IcObservationProviderError,
20        ic_snapshot_transfer_read::IcSnapshotTransferReadProvider,
21    },
22};
23use thiserror::Error;
24
25/// Durably reserve one original planned read, freshly admit it and invoke its provider once.
26///
27/// Hold no attempt guards on entry. The selected original journal remains locked
28/// through admission, dispatch and passive response association. Missing originals,
29/// pending attempts and unfulfilled prerequisites reject before provider invocation.
30/// This only accepts a newly reserved attempt; reconstructed pending requests are
31/// never dispatched. Every failure after reservation retains its consumed allowance.
32///
33/// `admit` must qualify actual fresh method-specific access, original metadata/raw-ID
34/// custody, current application requirements and exclusive never-dispatched command
35/// custody for this exact request. There is no default admission. Any remote preflight
36/// calls require their own prior accounting. The provider retains its existing
37/// authenticated single-update contract, without retries or hidden observations.
38///
39/// The returned bounded reply has only structural association. Integrations must
40/// independently authenticate it and explicitly use the existing journal receipt
41/// owner to record a qualified outcome. Even success leaves the attempt pending.
42/// No artifact, complete-transfer, fence or terminal/reference-release proof follows.
43/// # Errors
44/// Rejects changed originals, payloads, spending, fresh admission, provider failure
45/// or malformed/mismatched replies. Replies returned before a later rejection are
46/// retained in the error. No failure grants a repeat call, refund or cleanup.
47pub fn read_snapshot<E: std::error::Error + 'static>(
48    stage: &ExecutionStageGuard<'_>,
49    operation_sequence: u64,
50    payload: IcSnapshotTransferReadPayload<'_, '_>,
51    provider: &mut impl IcSnapshotTransferReadProvider,
52    admit: impl FnOnce(&IcSnapshotTransferReadRequest<'_, '_>) -> Result<(), E>,
53) -> Result<IcSnapshotTransferReadResponse, IcSnapshotTransferReadExecutionError<E>> {
54    let plan = stage.plan();
55    let authority = plan.attempt_authority(operation_sequence)?;
56    payload.validate_binding(&authority)?;
57    let mut journal = AttemptJournalGuard::open(stage.layout()?, &authority)?;
58    journal.reserve_planned_mutation(&plan.digest())?;
59    let request =
60        IcSnapshotTransferReadRequest::new(plan, operation_sequence, journal.record()?, payload)?;
61    admit(&request).map_err(IcSnapshotTransferReadExecutionError::Admission)?;
62    stage.layout()?;
63    let response = provider.read_snapshot(&request)?;
64    let association = match journal.record() {
65        Ok(record) => record,
66        Err(source) => {
67            return Err(IcSnapshotTransferReadExecutionError::AfterReplyJournal {
68                source,
69                response: Box::new(response),
70            });
71        }
72    };
73    if let Err(source) = validate_response(&request, association, &response) {
74        return Err(IcSnapshotTransferReadExecutionError::Association {
75            source,
76            response: Box::new(response),
77        });
78    }
79    if let Err(source) = stage.layout() {
80        return Err(IcSnapshotTransferReadExecutionError::AfterReplyStage {
81            source,
82            response: Box::new(response),
83        });
84    }
85    Ok(response)
86}
87
88/// Typed step failures retain original spending; returned raw replies remain available.
89#[derive(Debug, Error)]
90pub enum IcSnapshotTransferReadExecutionError<E: std::error::Error + 'static> {
91    /// The original operation cannot be derived.
92    #[error(transparent)]
93    Plan(#[from] OperationPlanError),
94    /// Retained workflow, stage or ancestors changed before dispatch.
95    #[error(transparent)]
96    Stage(#[from] ExecutionWorkflowPersistenceError),
97    /// Original journal admission or durable reservation failed.
98    #[error(transparent)]
99    Journal(#[from] AttemptJournalError),
100    /// Complete original progress, prerequisites or reservation failed.
101    #[error(transparent)]
102    Progress(#[from] ExecutionProgressPersistenceError),
103    /// Exact original payload/reservation admission failed.
104    #[error(transparent)]
105    Request(#[from] IcSnapshotTransferReadError),
106    /// Integration-owned fresh admission failed after durable reservation.
107    #[error("fresh snapshot read admission failed: {0}")]
108    Admission(#[source] E),
109    /// The single provider call failed; its reservation stays pending.
110    #[error(transparent)]
111    Provider(#[from] IcObservationProviderError),
112    /// Passive association rejected a bounded returned reply.
113    #[error("snapshot read response association failed: {source}")]
114    Association {
115        /// Canonical structural rejection.
116        source: IcSnapshotTransferReadAssociationError,
117        /// Exact bounded returned response, without authentication or outcome.
118        response: Box<IcSnapshotTransferReadResponse>,
119    },
120    /// Journal re-admission failed after a reply; retain the reply.
121    #[error("snapshot read journal changed after reply: {source}")]
122    AfterReplyJournal {
123        /// Original journal rejection.
124        source: AttemptJournalError,
125        /// Exact bounded returned response.
126        response: Box<IcSnapshotTransferReadResponse>,
127    },
128    /// Stage/ancestor re-admission failed after a reply; retain the reply.
129    #[error("snapshot read stage changed after reply: {source}")]
130    AfterReplyStage {
131        /// Original stage or ancestor rejection.
132        source: ExecutionWorkflowPersistenceError,
133        /// Exact bounded returned response.
134        response: Box<IcSnapshotTransferReadResponse>,
135    },
136}
137
138#[cfg(all(test, unix))]
139mod tests;