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