ic_backup/ops/persistence/attempt_journal/
mod.rs1use super::json::check_json_size;
4use super::{
5 BackupLayoutGuard, ExecutionProgressPersistenceError, JournalLock, JournalLockError,
6 PersistenceError, create_json_durable, read_json, write_json_durable,
7};
8use crate::model::artifacts::ArtifactChecksumRecord;
9use crate::model::attempt_journal::{
10 AttemptAuthorityRecord, AttemptJournalRecord, AttemptJournalRecordError,
11 MAX_ATTEMPT_JOURNAL_BYTES, MutationReceiptRequest, ObservationReceiptRequest,
12};
13use std::path::{Path, PathBuf};
14use thiserror::Error;
15
16#[derive(Debug)]
21pub struct AttemptJournalGuard<'a> {
22 layout: &'a BackupLayoutGuard,
23 _lock: JournalLock,
24 record: AttemptJournalRecord,
25 usable: bool,
26}
27
28impl<'a> AttemptJournalGuard<'a> {
29 pub fn create(
34 layout: &'a BackupLayoutGuard,
35 authority: AttemptAuthorityRecord,
36 ) -> Result<Self, AttemptJournalError> {
37 layout.check_root()?;
38 let path = journal_path(layout, &authority);
39 let lock = JournalLock::acquire(&path)?;
40 let record = AttemptJournalRecord::new(authority);
41 check_json_size(&record, MAX_ATTEMPT_JOURNAL_BYTES)?;
42 create_json_durable(&path, &record)?;
43 Ok(Self {
44 layout,
45 _lock: lock,
46 record,
47 usable: true,
48 })
49 }
50 pub fn open(
55 layout: &'a BackupLayoutGuard,
56 expected: &AttemptAuthorityRecord,
57 ) -> Result<Self, AttemptJournalError> {
58 layout.check_root()?;
59 let path = journal_path(layout, expected);
60 let lock = JournalLock::acquire(&path)?;
61 let record: AttemptJournalRecord = read_json(&path, MAX_ATTEMPT_JOURNAL_BYTES)?;
62 check_json_size(&record, MAX_ATTEMPT_JOURNAL_BYTES)?;
63 if record.authority() != expected {
64 return Err(AttemptJournalError::AuthorityMismatch);
65 }
66 Ok(Self {
67 layout,
68 _lock: lock,
69 record,
70 usable: true,
71 })
72 }
73 pub fn record(&self) -> Result<&AttemptJournalRecord, AttemptJournalError> {
78 self.check_usable()?;
79 Ok(&self.record)
80 }
81 #[must_use]
83 pub fn path(&self) -> PathBuf {
84 journal_path(self.layout, self.record.authority())
85 }
86 pub fn reserve_mutation(&mut self) -> Result<u32, AttemptJournalError> {
91 self.reserve_with(AttemptJournalRecord::reserve_mutation, write_json_durable)
92 }
93 pub fn reserve_planned_mutation(
104 &mut self,
105 expected_plan: &ArtifactChecksumRecord,
106 ) -> Result<u32, ExecutionProgressPersistenceError> {
107 let view = super::execution_progress::read_with_current(
108 self.layout,
109 expected_plan,
110 Some(self.record()?),
111 )?;
112 let sequence = self.record.authority().binding().operation_sequence();
113 if view.operations.iter().any(|operation| {
114 operation.operation_sequence == sequence
115 && operation.state
116 == crate::policy::execution_progress::OperationProgressState::AwaitingDependencies
117 }) {
118 return Err(ExecutionProgressPersistenceError::DependenciesUnapplied(sequence));
119 }
120 Ok(self.reserve_mutation()?)
121 }
122 pub fn reserve_observation(
128 &mut self,
129 mutation: u32,
130 request: &str,
131 ) -> Result<u32, AttemptJournalError> {
132 self.reserve_with(
133 |record| record.reserve_observation(mutation, request),
134 write_json_durable,
135 )
136 }
137 pub fn reserve_planned_observation(
147 &mut self,
148 expected_plan: &ArtifactChecksumRecord,
149 mutation: u32,
150 request: &str,
151 ) -> Result<u32, ExecutionProgressPersistenceError> {
152 super::execution_progress::read_with_current(
153 self.layout,
154 expected_plan,
155 Some(self.record()?),
156 )?;
157 Ok(self.reserve_observation(mutation, request)?)
158 }
159 pub fn record_mutation(
164 &mut self,
165 receipt: MutationReceiptRequest,
166 ) -> Result<(), AttemptJournalError> {
167 self.reserve_with(|record| record.record_mutation(receipt), write_json_durable)
168 }
169 pub fn record_observation(
177 &mut self,
178 receipt: ObservationReceiptRequest,
179 ) -> Result<(), AttemptJournalError> {
180 self.reserve_with(
181 |record| record.record_observation(receipt),
182 write_json_durable,
183 )
184 }
185
186 pub fn record_planned_observation(
195 &mut self,
196 expected_plan: &ArtifactChecksumRecord,
197 receipt: ObservationReceiptRequest,
198 ) -> Result<(), ExecutionProgressPersistenceError> {
199 self.admit_planned_progress(expected_plan)?;
200 Ok(self.record_observation(receipt)?)
201 }
202
203 pub(crate) fn admit_planned_progress(
204 &self,
205 expected_plan: &ArtifactChecksumRecord,
206 ) -> Result<(), ExecutionProgressPersistenceError> {
207 super::execution_progress::read_with_current(
208 self.layout,
209 expected_plan,
210 Some(self.record()?),
211 )?;
212 Ok(())
213 }
214
215 fn reserve_with<T>(
216 &mut self,
217 transition: impl FnOnce(&mut AttemptJournalRecord) -> Result<T, AttemptJournalRecordError>,
218 write: impl FnOnce(&Path, &AttemptJournalRecord) -> Result<(), PersistenceError>,
219 ) -> Result<T, AttemptJournalError> {
220 self.check_usable()?;
221 let mut next = self.record.clone();
222 let result = transition(&mut next)?;
223 check_json_size(&next, MAX_ATTEMPT_JOURNAL_BYTES)?;
224 self.usable = false;
225 write(&self.path(), &next)?;
226 self.record = next;
227 self.usable = true;
228 Ok(result)
229 }
230 fn check_usable(&self) -> Result<(), AttemptJournalError> {
231 if !self.usable {
232 return Err(AttemptJournalError::IndeterminateWrite);
233 }
234 self.layout.check_root()?;
235 Ok(())
236 }
237}
238
239pub(super) fn read_original_journals(
240 layout: &BackupLayoutGuard,
241 authorities: &[AttemptAuthorityRecord],
242 current: Option<&AttemptJournalRecord>,
243) -> Result<Vec<AttemptJournalRecord>, AttemptJournalError> {
244 layout.check_root()?;
245 if let Some(current) = current
246 && !authorities.contains(current.authority())
247 {
248 return Err(AttemptJournalError::AuthorityMismatch);
249 }
250 let mut journals = Vec::with_capacity(authorities.len());
251 for authority in authorities {
252 if let Some(current) = current
253 && current.authority() == authority
254 {
255 journals.push(current.clone());
256 } else {
257 let guard = AttemptJournalGuard::open(layout, authority)?;
258 journals.push(guard.record()?.clone());
259 }
260 }
261 layout.check_root()?;
262 Ok(journals)
263}
264
265fn journal_path(layout: &BackupLayoutGuard, authority: &AttemptAuthorityRecord) -> PathBuf {
266 layout.root().join(format!(
267 "attempt-{}.json",
268 authority.binding().operation_sequence()
269 ))
270}
271
272#[derive(Debug, Error)]
274pub enum AttemptJournalError {
275 #[error("attempt journal authority mismatch")]
277 AuthorityMismatch,
278 #[error("attempt journal write outcome indeterminate; reopen retained evidence")]
280 IndeterminateWrite,
281 #[error(transparent)]
283 Record(#[from] AttemptJournalRecordError),
284 #[error(transparent)]
286 Lock(#[from] JournalLockError),
287 #[error(transparent)]
289 Persistence(#[from] PersistenceError),
290}
291
292#[cfg(all(test, unix))]
293mod tests;