Skip to main content

ic_backup/workflow/fence_reconciliation/
mod.rs

1//! One original acquisition observation under retained stage and obligation custody.
2
3use crate::{
4    model::{
5        artifacts::ArtifactChecksumRecord,
6        attempt_journal::{ObservationOutcomeRecord, ObservationReceiptRequest},
7        fence_acquisition::{FenceAcquisitionError, FenceAcquisitionPayload, original_authority},
8        fence_obligation::FenceObligationRecord,
9        fence_reconciliation::{
10            FenceReconciliationIntent, FenceReconciliationObservation, FenceReconciliationRequest,
11            FenceReconciliationRequestError,
12        },
13        operation_plan::OperationPlanError,
14    },
15    ops::persistence::{
16        AttemptJournalError, AttemptJournalGuard, ExecutionProgressPersistenceError,
17        ExecutionStageGuard, ExecutionWorkflowPersistenceError, FenceObligationPersistenceError,
18        FenceObligationRequirement, read_fence_obligation,
19    },
20    policy::fence_reconciliation::{FenceReconciliationError, validate},
21    ports::fence_reconciliation::{FenceReconciliationProvider, FenceReconciliationProviderError},
22};
23use thiserror::Error;
24
25/// Reserve and observe one exact pending acquisition with mandatory fresh application admission.
26///
27/// Hold no attempt guards on entry. Retain the original stage, complete journals,
28/// acquisition bytes, requirement and obligation; restore retains both source and
29/// restore plans/layouts and outside-snapshot custody. Original payload admission
30/// precedes spending. Complete original-plan admission durably reserves one new
31/// observation; selected journal exclusion spans admission and the provider await.
32///
33/// `admit` qualifies actual current read authority, context, whole-unit membership,
34/// exact application observation semantics and never-dispatched custody for this
35/// reservation. Any remote preflight needs separate prior accounting. The provider
36/// retains/authenticates original request and result evidence and independently
37/// qualifies exclusive attribution, nonapplication or settled uncertainty. An Active
38/// fence or its current absence alone cannot qualify either acquisition outcome.
39///
40/// Success only returns matched passive evidence and leaves both attempts pending.
41/// Integration-owned durable retention and explicit settlement remain necessary.
42/// Cancellation, admission/provider failure or lost replies retain spending and
43/// deny reentry while the observation is pending. This step never reconstructs and
44/// redispatches a pending observation, retries acquisition, writes a receipt, refunds
45/// allowance or releases a fence/reference. No default application provider exists.
46/// # Errors
47/// Rejects missing/changed originals, exact payload or pending mutation, reservation,
48/// fresh admission, provider and passive association failures. Errors after a reply
49/// retain that complete observation; failures never authorize another dispatch.
50pub async fn observe_fence_acquisition<E: std::error::Error + 'static>(
51    stage: &ExecutionStageGuard<'_>,
52    requirement: FenceObligationRequirement<'_>,
53    obligation: &FenceObligationRecord,
54    payload: &FenceAcquisitionPayload,
55    challenge: ArtifactChecksumRecord,
56    provider: &mut impl FenceReconciliationProvider,
57    admit: impl AsyncFnOnce(&FenceReconciliationRequest<'_, '_>) -> Result<(), E>,
58) -> Result<FenceReconciliationObservation, FenceReconciliationExecutionError<E>> {
59    let plan = stage.plan();
60    let layout = stage.layout()?;
61    read_fence_obligation(layout, plan, requirement, &obligation.digest())?;
62    let authority = plan.attempt_authority(obligation.acquisition_operation())?;
63    let mut journal = AttemptJournalGuard::open(layout, &authority)?;
64    original_authority(plan, obligation, journal.record()?, payload)?;
65    let intent = FenceReconciliationIntent::new(plan, obligation, journal.record()?, challenge)?;
66    journal.reserve_planned_observation(
67        &plan.digest(),
68        intent.mutation_attempt(),
69        intent.digest().hash(),
70    )?;
71    let request = FenceReconciliationRequest::new(&intent, journal.record()?)?;
72    admit(&request)
73        .await
74        .map_err(FenceReconciliationExecutionError::Admission)?;
75    read_fence_obligation(stage.layout()?, plan, requirement, &obligation.digest())?;
76    let observation = provider
77        .observe_fence_acquisition(&request, journal.record()?)
78        .await?;
79    let association = match journal.record() {
80        Ok(record) => record,
81        Err(source) => {
82            return Err(FenceReconciliationExecutionError::AfterReplyJournal {
83                source,
84                observation: Box::new(observation),
85            });
86        }
87    };
88    if let Err(source) = validate(&request, association, &observation) {
89        return Err(FenceReconciliationExecutionError::Association {
90            source,
91            observation: Box::new(observation),
92        });
93    }
94    let layout = match stage.layout() {
95        Ok(layout) => layout,
96        Err(source) => {
97            return Err(FenceReconciliationExecutionError::AfterReplyStage {
98                source,
99                observation: Box::new(observation),
100            });
101        }
102    };
103    if let Err(source) = read_fence_obligation(layout, plan, requirement, &obligation.digest()) {
104        return Err(FenceReconciliationExecutionError::AfterReplyObligation {
105            source,
106            observation: Box::new(observation),
107        });
108    }
109    Ok(observation)
110}
111
112/// Explicitly retain an independently qualified original fence-acquisition observation.
113///
114/// Supply the original challenge and retained returned observation, including a late
115/// reply. Hold no attempt guards on entry. This opens the exact existing journal and
116/// admits the complete original plan/journal set before qualification and recording.
117/// Both original attempts must still be pending; no provider is called or reservation
118/// added. Selected journal exclusion spans the asynchronous qualification callback.
119///
120/// `qualify` independently authenticates the completed observation and durably retained
121/// request/result evidence, exclusive attribution or nonapplication/settled uncertainty,
122/// and actual whole-unit/outside-snapshot custody. Return the exact matched attempt,
123/// request, outcome and observation evidence. Passive matching alone supplies no receipt.
124/// Any remote qualification requires separate prior accounting outside this step.
125///
126/// Success delegates one receipt to the existing journal owner and retains all original
127/// allowances, obligations and source references. It supplies no current Active custody,
128/// capture/load/start permission, checkpoint, fence/reference release or prune admission.
129/// Settled uncertainty closes the observation while keeping the mutation pending.
130/// Cancellation before recording keeps both attempts pending. A write or post-write
131/// admission failure may leave a retained receipt; reopen original accounting to resolve
132/// it rather than repeating an observation or assuming that no settlement occurred.
133/// The borrowed observation remains available on every failure.
134/// # Errors
135/// Rejects changed originals, payload/reservation/result mismatch, missing/held journals,
136/// qualification or exact receipt mismatch, and durable-recording failures.
137pub async fn settle_fence_acquisition_observation<E: std::error::Error + 'static>(
138    stage: &ExecutionStageGuard<'_>,
139    requirement: FenceObligationRequirement<'_>,
140    obligation: &FenceObligationRecord,
141    payload: &FenceAcquisitionPayload,
142    challenge: ArtifactChecksumRecord,
143    observation: &FenceReconciliationObservation,
144    qualify: impl AsyncFnOnce(
145        &FenceReconciliationRequest<'_, '_>,
146        &FenceReconciliationObservation,
147    ) -> Result<ObservationReceiptRequest, E>,
148) -> Result<ObservationOutcomeRecord, FenceReconciliationSettlementError<E>> {
149    let plan = stage.plan();
150    let layout = stage.layout()?;
151    read_fence_obligation(layout, plan, requirement, &obligation.digest())?;
152    let authority = plan.attempt_authority(obligation.acquisition_operation())?;
153    let mut journal = AttemptJournalGuard::open(layout, &authority)?;
154    original_authority(plan, obligation, journal.record()?, payload)?;
155    journal.admit_planned_progress(&plan.digest())?;
156    let intent = FenceReconciliationIntent::new(plan, obligation, journal.record()?, challenge)?;
157    let request = FenceReconciliationRequest::new(&intent, journal.record()?)?;
158    let admitted = validate(&request, journal.record()?, observation)?;
159    let receipt = qualify(&request, observation)
160        .await
161        .map_err(FenceReconciliationSettlementError::Qualification)?;
162    if receipt.attempt != admitted.observation_attempt()
163        || receipt.request != admitted.request().hash()
164        || receipt.outcome != admitted.outcome()
165        || receipt.evidence != admitted.evidence().hash()
166    {
167        return Err(FenceReconciliationSettlementError::ReceiptMismatch);
168    }
169    validate(&request, journal.record()?, observation)?;
170    read_fence_obligation(stage.layout()?, plan, requirement, &obligation.digest())?;
171    let outcome = receipt.outcome;
172    journal.record_planned_observation(&plan.digest(), receipt)?;
173    read_fence_obligation(stage.layout()?, plan, requirement, &obligation.digest())?;
174    Ok(outcome)
175}
176
177/// Explicit settlement rejection; no error permits observation reissue or evidence cleanup.
178#[derive(Debug, Error)]
179pub enum FenceReconciliationSettlementError<E: std::error::Error + 'static> {
180    /// Original acquisition authority cannot be derived.
181    #[error(transparent)]
182    Plan(#[from] OperationPlanError),
183    /// Retained stage or ancestor evidence differs.
184    #[error(transparent)]
185    Stage(#[from] ExecutionWorkflowPersistenceError),
186    /// Original requirement or obligation cannot be admitted.
187    #[error(transparent)]
188    Obligation(#[from] FenceObligationPersistenceError),
189    /// Existing selected journal cannot be admitted.
190    #[error(transparent)]
191    Journal(#[from] AttemptJournalError),
192    /// Original acquisition payload or authority differs.
193    #[error(transparent)]
194    Payload(#[from] FenceAcquisitionError),
195    /// Complete original accounting or durable receipt publication failed.
196    #[error(transparent)]
197    Progress(#[from] ExecutionProgressPersistenceError),
198    /// Original pending mutation/observation identity differs.
199    #[error(transparent)]
200    Request(#[from] FenceReconciliationRequestError),
201    /// Passive exact result matching failed.
202    #[error(transparent)]
203    Association(#[from] FenceReconciliationError),
204    /// Independent application qualification rejected; original attempts stay pending.
205    #[error("fence observation settlement qualification failed: {0}")]
206    Qualification(#[source] E),
207    /// Qualified receipt does not retain the exact matched observation.
208    #[error("fence observation settlement receipt mismatch")]
209    ReceiptMismatch,
210}
211
212/// Observation-step failure preserving pending attempts, original obligations and replies.
213#[derive(Debug, Error)]
214pub enum FenceReconciliationExecutionError<E: std::error::Error + 'static> {
215    /// Original acquisition authority cannot be derived.
216    #[error(transparent)]
217    Plan(#[from] OperationPlanError),
218    /// Retained stage or ancestor evidence differs.
219    #[error(transparent)]
220    Stage(#[from] ExecutionWorkflowPersistenceError),
221    /// Original purpose-specific requirement or obligation cannot be admitted.
222    #[error(transparent)]
223    Obligation(#[from] FenceObligationPersistenceError),
224    /// Original selected journal admission failed.
225    #[error(transparent)]
226    Journal(#[from] AttemptJournalError),
227    /// Original acquisition bytes or authority differ.
228    #[error(transparent)]
229    Payload(#[from] FenceAcquisitionError),
230    /// Complete journals or durable observation reservation failed.
231    #[error(transparent)]
232    Progress(#[from] ExecutionProgressPersistenceError),
233    /// Original pending mutation or current observation binding differs.
234    #[error(transparent)]
235    Request(#[from] FenceReconciliationRequestError),
236    /// Fresh application admission rejected after reservation.
237    #[error("fresh fence reconciliation admission failed: {0}")]
238    Admission(#[source] E),
239    /// The one provider invocation failed; the observation remains pending.
240    #[error(transparent)]
241    Provider(#[from] FenceReconciliationProviderError),
242    /// Passive result association rejected the returned observation.
243    #[error("fence reconciliation association failed: {source}")]
244    Association {
245        /// Original result mismatch.
246        source: FenceReconciliationError,
247        /// Complete returned observation, without authenticated settlement authority.
248        observation: Box<FenceReconciliationObservation>,
249    },
250    /// Selected journal re-admission failed after reply.
251    #[error("fence reconciliation journal changed after reply: {source}")]
252    AfterReplyJournal {
253        /// Original journal rejection.
254        source: AttemptJournalError,
255        /// Complete returned observation.
256        observation: Box<FenceReconciliationObservation>,
257    },
258    /// Retained stage/ancestor admission failed after reply.
259    #[error("fence reconciliation stage changed after reply: {source}")]
260    AfterReplyStage {
261        /// Original stage/ancestor rejection.
262        source: ExecutionWorkflowPersistenceError,
263        /// Complete returned observation.
264        observation: Box<FenceReconciliationObservation>,
265    },
266    /// Original requirement/obligation admission failed after reply.
267    #[error("fence reconciliation obligation changed after reply: {source}")]
268    AfterReplyObligation {
269        /// Original requirement/obligation rejection.
270        source: FenceObligationPersistenceError,
271        /// Complete returned observation.
272        observation: Box<FenceReconciliationObservation>,
273    },
274}