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