ic_backup/ops/persistence/attempt_journal/
mod.rs1use super::json::{create_encoded, encode_json, read_record, write_encoded};
4use super::{
5 BackupLayoutGuard, ExecutionProgressPersistenceError, JournalLock, JournalLockError,
6 OperationPlanPersistenceError, PersistenceError, read_operation_plan,
7};
8use crate::model::artifacts::ArtifactChecksumRecord;
9use crate::model::attempt_journal::{
10 AttemptAuthorityRecord, AttemptJournalRecord, AttemptJournalRecordError,
11 MAX_ATTEMPT_JOURNAL_BYTES, MutationReceiptRequest, ObservationReceiptRequest,
12};
13use crate::model::operation_plan::{OperationPlanError, OperationPlanRecord};
14use std::path::{Path, PathBuf};
15use thiserror::Error;
16
17#[derive(Debug)]
22pub struct AttemptJournalGuard<'a> {
23 layout: &'a BackupLayoutGuard,
24 _lock: JournalLock,
25 record: AttemptJournalRecord,
26 usable: bool,
27}
28
29impl<'a> AttemptJournalGuard<'a> {
30 pub fn create(
35 layout: &'a BackupLayoutGuard,
36 authority: AttemptAuthorityRecord,
37 ) -> Result<Self, AttemptJournalError> {
38 layout.check_root()?;
39 let path = journal_path(layout, &authority);
40 let lock = JournalLock::acquire(&path)?;
41 let record = AttemptJournalRecord::new(authority);
42 let bytes = encode_json(&record, MAX_ATTEMPT_JOURNAL_BYTES)?;
43 create_encoded(&path, &bytes)?;
44 Ok(Self {
45 layout,
46 _lock: lock,
47 record,
48 usable: true,
49 })
50 }
51 pub fn open(
56 layout: &'a BackupLayoutGuard,
57 expected: &AttemptAuthorityRecord,
58 ) -> Result<Self, AttemptJournalError> {
59 layout.check_root()?;
60 let path = journal_path(layout, expected);
61 let lock = JournalLock::acquire(&path)?;
62 let record: AttemptJournalRecord = read_record(&path, 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
82 fn into_record(self) -> Result<AttemptJournalRecord, AttemptJournalError> {
83 self.check_usable()?;
84 Ok(self.record)
85 }
86 #[must_use]
88 pub fn path(&self) -> PathBuf {
89 journal_path(self.layout, self.record.authority())
90 }
91 pub(crate) fn reserve_mutation(&mut self) -> Result<u32, AttemptJournalError> {
96 self.reserve_with(AttemptJournalRecord::reserve_mutation, write_encoded)
97 }
98 pub fn reserve_planned_mutation(
109 &mut self,
110 expected_plan: &ArtifactChecksumRecord,
111 ) -> Result<u32, ExecutionProgressPersistenceError> {
112 super::execution_progress::admit_with_current(
113 self.layout,
114 expected_plan,
115 self.record()?,
116 true,
117 )?;
118 Ok(self.reserve_mutation()?)
119 }
120 pub(crate) fn reserve_observation(
126 &mut self,
127 mutation: u32,
128 request: &str,
129 ) -> Result<u32, AttemptJournalError> {
130 self.reserve_with(
131 |record| record.reserve_observation(mutation, request),
132 write_encoded,
133 )
134 }
135 pub fn reserve_planned_observation(
145 &mut self,
146 expected_plan: &ArtifactChecksumRecord,
147 mutation: u32,
148 request: &str,
149 ) -> Result<u32, ExecutionProgressPersistenceError> {
150 self.admit_planned_progress(expected_plan)?;
151 Ok(self.reserve_observation(mutation, request)?)
152 }
153 pub fn record_mutation(
158 &mut self,
159 receipt: MutationReceiptRequest,
160 ) -> Result<(), AttemptJournalError> {
161 self.reserve_with(|record| record.record_mutation(receipt), write_encoded)
162 }
163 pub fn record_observation(
171 &mut self,
172 receipt: ObservationReceiptRequest,
173 ) -> Result<(), AttemptJournalError> {
174 self.reserve_with(|record| record.record_observation(receipt), write_encoded)
175 }
176
177 pub fn record_planned_observation(
186 &mut self,
187 expected_plan: &ArtifactChecksumRecord,
188 receipt: ObservationReceiptRequest,
189 ) -> Result<(), ExecutionProgressPersistenceError> {
190 self.admit_planned_progress(expected_plan)?;
191 Ok(self.record_observation(receipt)?)
192 }
193
194 pub(crate) fn admit_planned_progress(
195 &self,
196 expected_plan: &ArtifactChecksumRecord,
197 ) -> Result<(), ExecutionProgressPersistenceError> {
198 super::execution_progress::admit_with_current(
199 self.layout,
200 expected_plan,
201 self.record()?,
202 false,
203 )?;
204 Ok(())
205 }
206
207 fn reserve_with<T>(
208 &mut self,
209 transition: impl FnOnce(&mut AttemptJournalRecord) -> Result<T, AttemptJournalRecordError>,
210 write: impl FnOnce(&Path, &[u8]) -> Result<(), PersistenceError>,
211 ) -> Result<T, AttemptJournalError> {
212 self.check_usable()?;
213 let mut next = self.record.clone();
214 let result = transition(&mut next)?;
215 let bytes = encode_json(&next, MAX_ATTEMPT_JOURNAL_BYTES)?;
216 let path = self.path();
217 super::json::replace_cached_record(&mut self.record, &mut self.usable, next, || {
218 write(&path, &bytes)
219 })?;
220 Ok(result)
221 }
222 fn check_usable(&self) -> Result<(), AttemptJournalError> {
223 if !self.usable {
224 return Err(AttemptJournalError::IndeterminateWrite);
225 }
226 self.layout.check_root()?;
227 Ok(())
228 }
229}
230
231pub(super) fn read_original_journals(
232 layout: &BackupLayoutGuard,
233 authorities: &[AttemptAuthorityRecord],
234 current: Option<&AttemptJournalRecord>,
235) -> Result<Vec<AttemptJournalRecord>, AttemptJournalError> {
236 layout.check_root()?;
237 if let Some(current) = current
238 && !authorities.contains(current.authority())
239 {
240 return Err(AttemptJournalError::AuthorityMismatch);
241 }
242 let mut journals = Vec::with_capacity(authorities.len());
243 for authority in authorities {
244 if let Some(current) = current
245 && current.authority() == authority
246 {
247 journals.push(current.clone());
248 } else {
249 let guard = AttemptJournalGuard::open(layout, authority)?;
250 journals.push(guard.into_record()?);
251 }
252 }
253 layout.check_root()?;
254 Ok(journals)
255}
256
257pub(super) fn read_original_plan_journals<E>(
258 layout: &BackupLayoutGuard,
259 expected_plan: &ArtifactChecksumRecord,
260 current: Option<&AttemptJournalRecord>,
261) -> Result<(OperationPlanRecord, Vec<AttemptJournalRecord>), E>
262where
263 E: From<OperationPlanPersistenceError> + From<OperationPlanError> + From<AttemptJournalError>,
264{
265 let plan = read_operation_plan(layout, expected_plan)?;
266 let authorities = plan.attempt_authorities()?;
267 let journals = read_original_journals(layout, &authorities, current)?;
268 Ok((plan, journals))
269}
270
271fn journal_path(layout: &BackupLayoutGuard, authority: &AttemptAuthorityRecord) -> PathBuf {
272 layout.root().join(format!(
273 "attempt-{}.json",
274 authority.binding().operation_sequence()
275 ))
276}
277
278#[derive(Debug, Error)]
280pub enum AttemptJournalError {
281 #[error("attempt journal authority mismatch")]
283 AuthorityMismatch,
284 #[error("attempt journal write outcome indeterminate; reopen retained evidence")]
286 IndeterminateWrite,
287 #[error(transparent)]
289 Record(#[from] AttemptJournalRecordError),
290 #[error(transparent)]
292 Lock(#[from] JournalLockError),
293 #[error(transparent)]
295 Persistence(#[from] PersistenceError),
296}
297
298#[cfg(all(test, unix))]
299mod tests;