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