Skip to main content

ic_backup/ops/persistence/fence_obligation/
mod.rs

1//! Bounded no-replace original fence obligations under retained plan/requirement custody.
2
3use super::{
4    BackupLayoutGuard, ConsistencyPersistenceError, JournalLock, JournalLockError,
5    PersistenceError, RestoreSafetyPersistenceError, create_json_durable,
6    read_consistency_requirement, read_json, read_restore_safety_requirement,
7};
8use crate::model::{
9    artifacts::ArtifactChecksumRecord,
10    consistency::{ApplicationFenceBinding, ConsistencyRequirementRecord},
11    fence_obligation::{FenceObligationError, FenceObligationRecord, MAX_FENCE_OBLIGATION_BYTES},
12    operation_plan::OperationPlanRecord,
13    restore_safety::RestoreSafetyRequirementRecord,
14};
15use thiserror::Error;
16
17/// Borrowed original declarations and guarded source custody used at the IO boundary.
18///
19/// These inputs grant no acquisition, Active fence, spending or release authority.
20#[derive(Clone, Copy, Debug)]
21pub enum FenceObligationRequirement<'a> {
22    /// Exact retained original coordinated capture declaration and chosen fence.
23    Capture {
24        /// Original requirement, already durably retained with its plan.
25        requirement: &'a ConsistencyRequirementRecord,
26        /// Exact original integration-retained identity and membership revision.
27        fence: &'a ApplicationFenceBinding,
28    },
29    /// Exact retained original restore/source declaration under both layout guards.
30    Restore {
31        /// Unchanged source layout exclusion.
32        source_layout: &'a BackupLayoutGuard,
33        /// Exact original source plan, already retained there.
34        source: &'a OperationPlanRecord,
35        /// Original fenced restore safety requirement, already durably retained.
36        requirement: &'a RestoreSafetyRequirementRecord,
37    },
38}
39impl FenceObligationRequirement<'_> {
40    fn validate(
41        self,
42        layout: &BackupLayoutGuard,
43        plan: &OperationPlanRecord,
44        record: &FenceObligationRecord,
45    ) -> Result<(), FenceObligationPersistenceError> {
46        match self {
47            Self::Capture { requirement, fence } => {
48                record.validate_capture(plan, requirement, fence)?;
49                read_consistency_requirement(layout, plan, &requirement.digest())?;
50            }
51            Self::Restore {
52                source_layout,
53                source,
54                requirement,
55            } => {
56                record.validate_restore(plan, source, requirement)?;
57                read_restore_safety_requirement(
58                    layout,
59                    source_layout,
60                    plan,
61                    source,
62                    &requirement.digest(),
63                )?;
64            }
65        }
66        Ok(())
67    }
68}
69/// Durably publish fixed `fence-obligation.json` without replacing retained obligations.
70///
71/// Publish before reservation/dispatch of its explicit acquisition operation.
72/// Attempt journals separately own every reservation, outcome and reconciliation.
73/// This function admits retained original declarations, not executable requests.
74/// # Errors
75/// Rejects absent/changed originals, inappropriate fence scope, existing/unsafe paths or IO.
76pub fn create_fence_obligation(
77    layout: &BackupLayoutGuard,
78    plan: &OperationPlanRecord,
79    requirement: FenceObligationRequirement<'_>,
80    record: &FenceObligationRecord,
81) -> Result<(), FenceObligationPersistenceError> {
82    requirement.validate(layout, plan, record)?;
83    let path = layout.root().join("fence-obligation.json");
84    let _lock = JournalLock::acquire(&path)?;
85    check_size(record)?;
86    create_json_durable(&path, record)?;
87    Ok(())
88}
89/// Read the exact bounded retained obligation; absence never recreates or releases it.
90///
91/// Lost local publication replies reconcile through this read. Recovery must also
92/// reopen the exact original acquisition journal; a missing journal is not an
93/// unspent allowance. No fresh provider call or fence action occurs here.
94/// # Errors
95/// Rejects changed originals, another digest, unsafe/missing paths or oversized/invalid bytes.
96pub fn read_fence_obligation(
97    layout: &BackupLayoutGuard,
98    plan: &OperationPlanRecord,
99    requirement: FenceObligationRequirement<'_>,
100    expected: &ArtifactChecksumRecord,
101) -> Result<FenceObligationRecord, FenceObligationPersistenceError> {
102    // Validate retained plans before following a location derived from the held layout.
103    super::read_operation_plan(layout, &plan.digest())?;
104    let path = layout.root().join("fence-obligation.json");
105    let _lock = JournalLock::acquire(&path)?;
106    let record: FenceObligationRecord = read_json(&path, MAX_FENCE_OBLIGATION_BYTES)?;
107    check_size(&record)?;
108    requirement.validate(layout, plan, &record)?;
109    if &record.digest() != expected {
110        return Err(FenceObligationPersistenceError::DigestMismatch);
111    }
112    Ok(record)
113}
114fn check_size(record: &FenceObligationRecord) -> Result<(), PersistenceError> {
115    super::json::check_json_size(record, MAX_FENCE_OBLIGATION_BYTES)
116}
117/// Typed denial preserving original obligation bytes and acquisition spending.
118#[derive(Debug, Error)]
119pub enum FenceObligationPersistenceError {
120    /// Retained obligation differs from the original exact expected digest.
121    #[error("fence obligation digest mismatch")]
122    DigestMismatch,
123    /// Exact original fence scope or plan differs.
124    #[error(transparent)]
125    Obligation(#[from] FenceObligationError),
126    /// Original plan cannot be read under its unchanged layout.
127    #[error(transparent)]
128    Plan(#[from] super::OperationPlanPersistenceError),
129    /// Exact original capture requirement cannot be read.
130    #[error(transparent)]
131    Consistency(#[from] ConsistencyPersistenceError),
132    /// Exact original restore/source requirement cannot be read.
133    #[error(transparent)]
134    Restore(#[from] RestoreSafetyPersistenceError),
135    /// Journal exclusion failed.
136    #[error(transparent)]
137    Lock(#[from] JournalLockError),
138    /// Bounded/durable filesystem or JSON operation failed.
139    #[error(transparent)]
140    Persistence(#[from] PersistenceError),
141}
142
143#[cfg(all(test, unix))]
144mod tests;