Skip to main content

ic_backup/ops/persistence/attempt_journal/
mod.rs

1//! Exclusive durable attempt reservations and exact receipt retention; no transport.
2
3use super::{
4    BackupLayoutGuard, JournalLock, JournalLockError, PersistenceError, create_json_durable,
5    read_json, write_json_durable,
6};
7use crate::model::attempt_journal::{
8    AttemptAuthorityRecord, AttemptJournalRecord, AttemptJournalRecordError,
9    MAX_ATTEMPT_JOURNAL_BYTES, MutationReceiptRequest, ObservationReceiptRequest,
10};
11use std::path::{Path, PathBuf};
12use thiserror::Error;
13
14/// Exclusive local accounting borrowing stable layout exclusion.
15///
16/// Successful reservation persists consumption before returning its number.
17/// It grants no fresh IC authority, backend settlement or subprocess dispatch permission.
18#[derive(Debug)]
19pub struct AttemptJournalGuard<'a> {
20    layout: &'a BackupLayoutGuard,
21    _lock: JournalLock,
22    record: AttemptJournalRecord,
23    usable: bool,
24}
25
26impl<'a> AttemptJournalGuard<'a> {
27    /// Create original exact identity and limits without replacing retained evidence.
28    ///
29    /// # Errors
30    /// Rejects unsafe/existing journals, lock contention, replaced layouts and failed persistence.
31    pub fn create(
32        layout: &'a BackupLayoutGuard,
33        authority: AttemptAuthorityRecord,
34    ) -> Result<Self, AttemptJournalError> {
35        layout.check_root()?;
36        let path = journal_path(layout, &authority);
37        let lock = JournalLock::acquire(&path)?;
38        let record = AttemptJournalRecord::new(authority);
39        check_size(&record)?;
40        create_json_durable(&path, &record)?;
41        Ok(Self {
42            layout,
43            _lock: lock,
44            record,
45            usable: true,
46        })
47    }
48    /// Open exact retained identity and original budgets, replaying only local events.
49    ///
50    /// # Errors
51    /// Rejects identity/budget changes, invalid chronology, missing/unsafe/oversized records and locks.
52    pub fn open(
53        layout: &'a BackupLayoutGuard,
54        expected: &AttemptAuthorityRecord,
55    ) -> Result<Self, AttemptJournalError> {
56        layout.check_root()?;
57        let path = journal_path(layout, expected);
58        let lock = JournalLock::acquire(&path)?;
59        let record: AttemptJournalRecord = read_json(&path, MAX_ATTEMPT_JOURNAL_BYTES)?;
60        check_size(&record)?;
61        if record.authority() != expected {
62            return Err(AttemptJournalError::AuthorityMismatch);
63        }
64        Ok(Self {
65            layout,
66            _lock: lock,
67            record,
68            usable: true,
69        })
70    }
71    /// Read validated retained accounting without observing remote state.
72    ///
73    /// # Errors
74    /// Rejects indeterminate writes or replaced layouts.
75    pub fn record(&self) -> Result<&AttemptJournalRecord, AttemptJournalError> {
76        self.check_usable()?;
77        Ok(&self.record)
78    }
79    /// Return the exact journal location for this operation sequence.
80    #[must_use]
81    pub fn path(&self) -> PathBuf {
82        journal_path(self.layout, self.record.authority())
83    }
84    /// Durably consume one mutation attempt before any admitted caller-owned call.
85    ///
86    /// # Errors
87    /// Rejects unresolved attempts, exhaustion, applied operations and persistence failures.
88    pub fn reserve_mutation(&mut self) -> Result<u32, AttemptJournalError> {
89        self.reserve_with(AttemptJournalRecord::reserve_mutation, write_json_durable)
90    }
91    /// Durably consume one observation bound to an exact unresolved mutation and request.
92    ///
93    /// Fresh authority, paid-effect settlement and ended command custody remain caller-owned.
94    /// # Errors
95    /// Rejects wrong attempts/requests, unresolved observations, exhaustion and failed writes.
96    pub fn reserve_observation(
97        &mut self,
98        mutation: u32,
99        request: &str,
100    ) -> Result<u32, AttemptJournalError> {
101        self.reserve_with(
102            |record| record.reserve_observation(mutation, request),
103            write_json_durable,
104        )
105    }
106    /// Retain an exact integration-qualified direct mutation reply.
107    ///
108    /// # Errors
109    /// Rejects unmatched identity, absent/unsettled attempts, invalid evidence and failed persistence.
110    pub fn record_mutation(
111        &mut self,
112        receipt: MutationReceiptRequest,
113    ) -> Result<(), AttemptJournalError> {
114        self.reserve_with(|record| record.record_mutation(receipt), write_json_durable)
115    }
116    /// Retain an exact observation; unresolved evidence never authorizes another mutation.
117    ///
118    /// The integration qualifies completed observation and paid-effect settlement.
119    /// A lost observation response stays pending until qualified settlement.
120    ///
121    /// # Errors
122    /// Rejects unmatched identity, absent/unsettled attempts, invalid evidence and failed persistence.
123    pub fn record_observation(
124        &mut self,
125        receipt: ObservationReceiptRequest,
126    ) -> Result<(), AttemptJournalError> {
127        self.reserve_with(
128            |record| record.record_observation(receipt),
129            write_json_durable,
130        )
131    }
132
133    fn reserve_with<T>(
134        &mut self,
135        transition: impl FnOnce(&mut AttemptJournalRecord) -> Result<T, AttemptJournalRecordError>,
136        write: impl FnOnce(&Path, &AttemptJournalRecord) -> Result<(), PersistenceError>,
137    ) -> Result<T, AttemptJournalError> {
138        self.check_usable()?;
139        let mut next = self.record.clone();
140        let result = transition(&mut next)?;
141        check_size(&next)?;
142        self.usable = false;
143        write(&self.path(), &next)?;
144        self.record = next;
145        self.usable = true;
146        Ok(result)
147    }
148    fn check_usable(&self) -> Result<(), AttemptJournalError> {
149        if !self.usable {
150            return Err(AttemptJournalError::IndeterminateWrite);
151        }
152        self.layout.check_root()?;
153        Ok(())
154    }
155}
156
157fn journal_path(layout: &BackupLayoutGuard, authority: &AttemptAuthorityRecord) -> PathBuf {
158    layout.root().join(format!(
159        "attempt-{}.json",
160        authority.binding().operation_sequence()
161    ))
162}
163fn check_size(record: &AttemptJournalRecord) -> Result<(), PersistenceError> {
164    super::json::check_json_size(record, MAX_ATTEMPT_JOURNAL_BYTES)
165}
166
167/// Typed exact admission, local accounting or durable publication failure.
168#[derive(Debug, Error)]
169pub enum AttemptJournalError {
170    /// Retained identity or original ceilings differ from current exact selection.
171    #[error("attempt journal authority mismatch")]
172    AuthorityMismatch,
173    /// A write may have completed; drop and reopen exact retained evidence.
174    #[error("attempt journal write outcome indeterminate; reopen retained evidence")]
175    IndeterminateWrite,
176    /// Model identity, chronology or allowance admission failed.
177    #[error(transparent)]
178    Record(#[from] AttemptJournalRecordError),
179    /// Journal/layout exclusion failed.
180    #[error(transparent)]
181    Lock(#[from] JournalLockError),
182    /// Bounded durable record access failed.
183    #[error(transparent)]
184    Persistence(#[from] PersistenceError),
185}
186
187#[cfg(all(test, unix))]
188mod tests;