Skip to main content

ic_backup/ops/persistence/execution_settlement/
mod.rs

1//! Immutable original all-Applied journal checkpoint publication and local replay.
2
3use super::json::check_json_size;
4use super::{
5    AttemptJournalError, BackupLayoutGuard, JournalLock, JournalLockError,
6    OperationPlanPersistenceError, PersistenceError, create_json_durable, read_json,
7};
8use crate::{
9    model::{
10        artifacts::ArtifactChecksumRecord,
11        execution_settlement::{
12            ExecutionSettlementError, ExecutionSettlementJournalRecord, ExecutionSettlementRecord,
13            MAX_EXECUTION_SETTLEMENT_BYTES,
14        },
15    },
16    policy::execution_settlement::{ExecutionSettlementPolicyError, validate},
17};
18use std::{io, path::Path};
19use thiserror::Error;
20
21/// Derive and publish an all-Applied checkpoint from the exact retained original journals.
22///
23/// Reads every original journal sequentially, then delegates immutable publication
24/// to [`create_execution_settlement`], which rechecks the complete original histories.
25/// Hold no journal guards at admission. This creates no receipts, spending allowance,
26/// product terminal proof or release permission. An occupied checkpoint is never replaced;
27/// use [`read_execution_settlement`] with its retained identity for local replay.
28/// # Errors
29/// Rejects missing, held, changed or unsettled originals and publication failures.
30pub fn checkpoint_execution_settlement(
31    layout: &BackupLayoutGuard,
32    expected_plan: &ArtifactChecksumRecord,
33) -> Result<ExecutionSettlementRecord, ExecutionSettlementCheckpointError> {
34    let record = derive_execution_settlement(layout, expected_plan)?;
35    create_execution_settlement(layout, &record)?;
36    Ok(record)
37}
38
39/// Reconcile a lost checkpoint response from complete original journals and the
40/// existing immutable checkpoint, without publishing or changing any evidence.
41///
42/// The derived digest is checked against the retained checkpoint, then its full
43/// original histories are admitted again by the existing reader. This local
44/// sequential replay supplies no authenticated receipt, terminal proof or release.
45/// # Errors
46/// Rejects absent/changed checkpoints, missing/held/unsettled journals and drift.
47pub fn replay_execution_settlement(
48    layout: &BackupLayoutGuard,
49    expected_plan: &ArtifactChecksumRecord,
50) -> Result<ExecutionSettlementRecord, ExecutionSettlementCheckpointError> {
51    let derived = derive_execution_settlement(layout, expected_plan)?;
52    Ok(read_execution_settlement(
53        layout,
54        expected_plan,
55        &derived.digest(),
56    )?)
57}
58
59/// Complete or replay the exact local checkpoint after an uncertain publication response.
60///
61/// Derive the expected identity from the complete retained original journals first.
62/// An existing exact checkpoint is read without replacement. Only a genuinely
63/// absent checkpoint may be created; changed or unsafe entries refuse. A new
64/// publication is read back through the same exact-original replay owner.
65/// This performs no provider call, receipt transition, refund, terminal admission
66/// or reference release. Hold no attempt guards on entry.
67/// # Errors
68/// Rejects incomplete/held originals, changed or unsafe occupied checkpoints and
69/// local publication/readback failures while retaining every original record.
70pub fn reconcile_execution_settlement(
71    layout: &BackupLayoutGuard,
72    expected_plan: &ArtifactChecksumRecord,
73) -> Result<ExecutionSettlementRecord, ExecutionSettlementCheckpointError> {
74    let derived = derive_execution_settlement(layout, expected_plan)?;
75    match read_execution_settlement(layout, expected_plan, &derived.digest()) {
76        Ok(existing) => Ok(existing),
77        Err(ExecutionSettlementPersistenceError::Persistence(PersistenceError::Io(error)))
78            if error.kind() == io::ErrorKind::NotFound =>
79        {
80            create_execution_settlement(layout, &derived)?;
81            Ok(read_execution_settlement(
82                layout,
83                expected_plan,
84                &derived.digest(),
85            )?)
86        }
87        Err(error) => Err(error.into()),
88    }
89}
90
91fn derive_execution_settlement(
92    layout: &BackupLayoutGuard,
93    expected_plan: &ArtifactChecksumRecord,
94) -> Result<ExecutionSettlementRecord, ExecutionSettlementCheckpointError> {
95    let (plan, journals) = super::attempt_journal::read_original_plan_journals::<
96        ExecutionSettlementPersistenceError,
97    >(layout, expected_plan, None)?;
98    let rows = journals
99        .iter()
100        .map(ExecutionSettlementJournalRecord::from_journal)
101        .collect();
102    Ok(ExecutionSettlementRecord::new(plan.digest(), rows)?)
103}
104
105/// Publish fixed `execution-settlement.json` under original retained plan/journal evidence.
106///
107/// The exclusive layout owns cooperating write exclusion. Journal locks are acquired
108/// sequentially in canonical sequence order, bounding descriptor use; every admitted
109/// Applied journal rejects further owner transitions. Hold no journal guards when
110/// invoking this operation. Noncooperating byte custody and authentic receipts remain
111/// integration-owned. This creates no journal, budget, product terminal or release permit.
112/// # Errors
113/// Rejects missing/unsafe/oversized originals, contention, unsettled/changed evidence or existing publication.
114pub fn create_execution_settlement(
115    layout: &BackupLayoutGuard,
116    record: &ExecutionSettlementRecord,
117) -> Result<(), ExecutionSettlementPersistenceError> {
118    create_with(layout, record, create_json_durable)
119}
120fn create_with(
121    layout: &BackupLayoutGuard,
122    record: &ExecutionSettlementRecord,
123    writer: impl FnOnce(&Path, &ExecutionSettlementRecord) -> Result<(), PersistenceError>,
124) -> Result<(), ExecutionSettlementPersistenceError> {
125    layout.check_root()?;
126    let path = layout.root().join("execution-settlement.json");
127    let _lock = JournalLock::acquire(&path)?;
128    check_json_size(record, MAX_EXECUTION_SETTLEMENT_BYTES)?;
129    validate_retained(layout, record)?;
130    writer(&path, record)?;
131    Ok(())
132}
133/// Reopen exact checkpoint and validate complete retained original evidence using local IO only.
134///
135/// A lost publication reply can be reconciled without rewriting evidence or observing
136/// remote state. This is original journal settlement replay, not fresh verification
137/// of artifacts/application state, full run completion or release admission.
138/// # Errors
139/// Rejects changed expected identity, absent/invalid/unsafe evidence, contention or journal drift.
140pub fn read_execution_settlement(
141    layout: &BackupLayoutGuard,
142    expected_plan: &ArtifactChecksumRecord,
143    expected: &ArtifactChecksumRecord,
144) -> Result<ExecutionSettlementRecord, ExecutionSettlementPersistenceError> {
145    layout.check_root()?;
146    let path = layout.root().join("execution-settlement.json");
147    let _lock = JournalLock::acquire(&path)?;
148    let record: ExecutionSettlementRecord = read_json(&path, MAX_EXECUTION_SETTLEMENT_BYTES)?;
149    check_json_size(&record, MAX_EXECUTION_SETTLEMENT_BYTES)?;
150    if record.plan_intent() != expected_plan || &record.digest() != expected {
151        return Err(ExecutionSettlementPersistenceError::DigestMismatch);
152    }
153    validate_retained(layout, &record)?;
154    Ok(record)
155}
156fn validate_retained(
157    layout: &BackupLayoutGuard,
158    record: &ExecutionSettlementRecord,
159) -> Result<(), ExecutionSettlementPersistenceError> {
160    let (plan, journals) = super::attempt_journal::read_original_plan_journals::<
161        ExecutionSettlementPersistenceError,
162    >(layout, record.plan_intent(), None)?;
163    let references: Vec<_> = journals.iter().collect();
164    validate(&plan, &references, record)?;
165    layout.check_root()?;
166    Ok(())
167}
168/// Failure deriving, publishing or replaying a checkpoint; originals remain retained.
169#[derive(Debug, Error)]
170pub enum ExecutionSettlementCheckpointError {
171    /// Original local evidence or immutable publication cannot be admitted.
172    #[error(transparent)]
173    Persistence(#[from] ExecutionSettlementPersistenceError),
174    /// The derived checkpoint cannot satisfy the existing bounded record schema.
175    #[error(transparent)]
176    Record(#[from] ExecutionSettlementError),
177}
178/// Typed immutable publication/replay failure, preserving all original journals and obligations.
179#[derive(Debug, Error)]
180pub enum ExecutionSettlementPersistenceError {
181    /// Expected original plan/checkpoint identity differs.
182    #[error("execution settlement digest mismatch")]
183    DigestMismatch,
184    /// Original retained plan cannot be admitted.
185    #[error(transparent)]
186    Plan(#[from] OperationPlanPersistenceError),
187    /// Original plan authority derivation failed.
188    #[error(transparent)]
189    Authority(#[from] crate::model::operation_plan::OperationPlanError),
190    /// An original journal is absent, unsafe, mismatched or held by another owner.
191    #[error(transparent)]
192    Journal(#[from] AttemptJournalError),
193    /// Complete original settlement evidence failed pure policy.
194    #[error(transparent)]
195    Policy(#[from] ExecutionSettlementPolicyError),
196    /// Publication/replay checkpoint lock failed.
197    #[error(transparent)]
198    Lock(#[from] JournalLockError),
199    /// Bounded local IO or canonical encoding failed.
200    #[error(transparent)]
201    Persistence(#[from] PersistenceError),
202}
203#[cfg(all(test, unix))]
204mod tests;