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::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 (plan, journals) = read_retained_journals(layout, expected_plan)?;
38    let rows = journals
39        .iter()
40        .map(ExecutionSettlementJournalRecord::from_journal)
41        .collect();
42    let record = ExecutionSettlementRecord::new(plan.digest(), rows)?;
43    create_execution_settlement(layout, &record)?;
44    Ok(record)
45}
46
47/// Publish fixed `execution-settlement.json` under original retained plan/journal evidence.
48///
49/// The exclusive layout owns cooperating write exclusion. Journal locks are acquired
50/// sequentially in canonical sequence order, bounding descriptor use; every admitted
51/// Applied journal rejects further owner transitions. Hold no journal guards when
52/// invoking this operation. Noncooperating byte custody and authentic receipts remain
53/// integration-owned. This creates no journal, budget, product terminal or release permit.
54/// # Errors
55/// Rejects missing/unsafe/oversized originals, contention, unsettled/changed evidence or existing publication.
56pub fn create_execution_settlement(
57    layout: &BackupLayoutGuard,
58    record: &ExecutionSettlementRecord,
59) -> Result<(), ExecutionSettlementPersistenceError> {
60    create_with(layout, record, create_json_durable)
61}
62fn create_with(
63    layout: &BackupLayoutGuard,
64    record: &ExecutionSettlementRecord,
65    writer: impl FnOnce(&Path, &ExecutionSettlementRecord) -> Result<(), PersistenceError>,
66) -> Result<(), ExecutionSettlementPersistenceError> {
67    layout.check_root()?;
68    let path = layout.root().join("execution-settlement.json");
69    let _lock = JournalLock::acquire(&path)?;
70    check_json_size(record, MAX_EXECUTION_SETTLEMENT_BYTES)?;
71    validate_retained(layout, record)?;
72    writer(&path, record)?;
73    Ok(())
74}
75/// Reopen exact checkpoint and validate complete retained original evidence using local IO only.
76///
77/// A lost publication reply can be reconciled without rewriting evidence or observing
78/// remote state. This is original journal settlement replay, not fresh verification
79/// of artifacts/application state, full run completion or release admission.
80/// # Errors
81/// Rejects changed expected identity, absent/invalid/unsafe evidence, contention or journal drift.
82pub fn read_execution_settlement(
83    layout: &BackupLayoutGuard,
84    expected_plan: &ArtifactChecksumRecord,
85    expected: &ArtifactChecksumRecord,
86) -> Result<ExecutionSettlementRecord, ExecutionSettlementPersistenceError> {
87    layout.check_root()?;
88    let path = layout.root().join("execution-settlement.json");
89    let _lock = JournalLock::acquire(&path)?;
90    let record: ExecutionSettlementRecord = read_json(&path, MAX_EXECUTION_SETTLEMENT_BYTES)?;
91    check_json_size(&record, MAX_EXECUTION_SETTLEMENT_BYTES)?;
92    if record.plan_intent() != expected_plan || &record.digest() != expected {
93        return Err(ExecutionSettlementPersistenceError::DigestMismatch);
94    }
95    validate_retained(layout, &record)?;
96    Ok(record)
97}
98fn validate_retained(
99    layout: &BackupLayoutGuard,
100    record: &ExecutionSettlementRecord,
101) -> Result<(), ExecutionSettlementPersistenceError> {
102    let (plan, journals) = read_retained_journals(layout, record.plan_intent())?;
103    let references: Vec<_> = journals.iter().collect();
104    validate(&plan, &references, record)?;
105    layout.check_root()?;
106    Ok(())
107}
108fn read_retained_journals(
109    layout: &BackupLayoutGuard,
110    expected_plan: &ArtifactChecksumRecord,
111) -> Result<(OperationPlanRecord, Vec<AttemptJournalRecord>), ExecutionSettlementPersistenceError> {
112    let plan = read_operation_plan(layout, expected_plan)?;
113    let authorities = plan.attempt_authorities()?;
114    let journals = super::attempt_journal::read_original_journals(layout, &authorities, None)?;
115    Ok((plan, journals))
116}
117/// Failure deriving or publishing a checkpoint; original records remain retained.
118#[derive(Debug, Error)]
119pub enum ExecutionSettlementCheckpointError {
120    /// Original local evidence or immutable publication cannot be admitted.
121    #[error(transparent)]
122    Persistence(#[from] ExecutionSettlementPersistenceError),
123    /// The derived checkpoint cannot satisfy the existing bounded record schema.
124    #[error(transparent)]
125    Record(#[from] ExecutionSettlementError),
126}
127/// Typed immutable publication/replay failure, preserving all original journals and obligations.
128#[derive(Debug, Error)]
129pub enum ExecutionSettlementPersistenceError {
130    /// Expected original plan/checkpoint identity differs.
131    #[error("execution settlement digest mismatch")]
132    DigestMismatch,
133    /// Original retained plan cannot be admitted.
134    #[error(transparent)]
135    Plan(#[from] OperationPlanPersistenceError),
136    /// Original plan authority derivation failed.
137    #[error(transparent)]
138    Authority(#[from] crate::model::operation_plan::OperationPlanError),
139    /// An original journal is absent, unsafe, mismatched or held by another owner.
140    #[error(transparent)]
141    Journal(#[from] AttemptJournalError),
142    /// Complete original settlement evidence failed pure policy.
143    #[error(transparent)]
144    Policy(#[from] ExecutionSettlementPolicyError),
145    /// Publication/replay checkpoint lock failed.
146    #[error(transparent)]
147    Lock(#[from] JournalLockError),
148    /// Bounded local IO or canonical encoding failed.
149    #[error(transparent)]
150    Persistence(#[from] PersistenceError),
151}
152#[cfg(all(test, unix))]
153mod tests;