ic_backup/ops/persistence/attempt_journal/
mod.rs1use super::json::{check_json_size, create_encoded, encode_json, write_encoded};
4use super::{
5 BackupLayoutGuard, ExecutionProgressPersistenceError, JournalLock, JournalLockError,
6 OperationPlanPersistenceError, PersistenceError, read_json, 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_json(&path, MAX_ATTEMPT_JOURNAL_BYTES)?;
63 check_json_size(&record, MAX_ATTEMPT_JOURNAL_BYTES)?;
64 if record.authority() != expected {
65 return Err(AttemptJournalError::AuthorityMismatch);
66 }
67 Ok(Self {
68 layout,
69 _lock: lock,
70 record,
71 usable: true,
72 })
73 }
74 pub fn record(&self) -> Result<&AttemptJournalRecord, AttemptJournalError> {
79 self.check_usable()?;
80 Ok(&self.record)
81 }
82 #[must_use]
84 pub fn path(&self) -> PathBuf {
85 journal_path(self.layout, self.record.authority())
86 }
87 pub fn reserve_mutation(&mut self) -> Result<u32, AttemptJournalError> {
92 self.reserve_with(AttemptJournalRecord::reserve_mutation, write_encoded)
93 }
94 pub fn reserve_planned_mutation(
105 &mut self,
106 expected_plan: &ArtifactChecksumRecord,
107 ) -> Result<u32, ExecutionProgressPersistenceError> {
108 let view = super::execution_progress::read_with_current(
109 self.layout,
110 expected_plan,
111 Some(self.record()?),
112 )?;
113 let sequence = self.record.authority().binding().operation_sequence();
114 if view.operations.iter().any(|operation| {
115 operation.operation_sequence == sequence
116 && operation.state
117 == crate::policy::execution_progress::OperationProgressState::AwaitingDependencies
118 }) {
119 return Err(ExecutionProgressPersistenceError::DependenciesUnapplied(sequence));
120 }
121 Ok(self.reserve_mutation()?)
122 }
123 pub fn reserve_observation(
129 &mut self,
130 mutation: u32,
131 request: &str,
132 ) -> Result<u32, AttemptJournalError> {
133 self.reserve_with(
134 |record| record.reserve_observation(mutation, request),
135 write_encoded,
136 )
137 }
138 pub fn reserve_planned_observation(
148 &mut self,
149 expected_plan: &ArtifactChecksumRecord,
150 mutation: u32,
151 request: &str,
152 ) -> Result<u32, ExecutionProgressPersistenceError> {
153 super::execution_progress::read_with_current(
154 self.layout,
155 expected_plan,
156 Some(self.record()?),
157 )?;
158 Ok(self.reserve_observation(mutation, request)?)
159 }
160 pub fn record_mutation(
165 &mut self,
166 receipt: MutationReceiptRequest,
167 ) -> Result<(), AttemptJournalError> {
168 self.reserve_with(|record| record.record_mutation(receipt), write_encoded)
169 }
170 pub fn record_observation(
178 &mut self,
179 receipt: ObservationReceiptRequest,
180 ) -> Result<(), AttemptJournalError> {
181 self.reserve_with(|record| record.record_observation(receipt), write_encoded)
182 }
183
184 pub fn record_planned_observation(
193 &mut self,
194 expected_plan: &ArtifactChecksumRecord,
195 receipt: ObservationReceiptRequest,
196 ) -> Result<(), ExecutionProgressPersistenceError> {
197 self.admit_planned_progress(expected_plan)?;
198 Ok(self.record_observation(receipt)?)
199 }
200
201 pub(crate) fn admit_planned_progress(
202 &self,
203 expected_plan: &ArtifactChecksumRecord,
204 ) -> Result<(), ExecutionProgressPersistenceError> {
205 super::execution_progress::read_with_current(
206 self.layout,
207 expected_plan,
208 Some(self.record()?),
209 )?;
210 Ok(())
211 }
212
213 fn reserve_with<T>(
214 &mut self,
215 transition: impl FnOnce(&mut AttemptJournalRecord) -> Result<T, AttemptJournalRecordError>,
216 write: impl FnOnce(&Path, &[u8]) -> Result<(), PersistenceError>,
217 ) -> Result<T, AttemptJournalError> {
218 self.check_usable()?;
219 let mut next = self.record.clone();
220 let result = transition(&mut next)?;
221 let bytes = encode_json(&next, MAX_ATTEMPT_JOURNAL_BYTES)?;
222 self.usable = false;
223 write(&self.path(), &bytes)?;
224 self.record = next;
225 self.usable = true;
226 Ok(result)
227 }
228 fn check_usable(&self) -> Result<(), AttemptJournalError> {
229 if !self.usable {
230 return Err(AttemptJournalError::IndeterminateWrite);
231 }
232 self.layout.check_root()?;
233 Ok(())
234 }
235}
236
237pub(super) fn read_original_journals(
238 layout: &BackupLayoutGuard,
239 authorities: &[AttemptAuthorityRecord],
240 current: Option<&AttemptJournalRecord>,
241) -> Result<Vec<AttemptJournalRecord>, AttemptJournalError> {
242 layout.check_root()?;
243 if let Some(current) = current
244 && !authorities.contains(current.authority())
245 {
246 return Err(AttemptJournalError::AuthorityMismatch);
247 }
248 let mut journals = Vec::with_capacity(authorities.len());
249 for authority in authorities {
250 if let Some(current) = current
251 && current.authority() == authority
252 {
253 journals.push(current.clone());
254 } else {
255 let guard = AttemptJournalGuard::open(layout, authority)?;
256 journals.push(guard.record()?.clone());
257 }
258 }
259 layout.check_root()?;
260 Ok(journals)
261}
262
263pub(super) fn read_original_plan_journals<E>(
264 layout: &BackupLayoutGuard,
265 expected_plan: &ArtifactChecksumRecord,
266 current: Option<&AttemptJournalRecord>,
267) -> Result<(OperationPlanRecord, Vec<AttemptJournalRecord>), E>
268where
269 E: From<OperationPlanPersistenceError> + From<OperationPlanError> + From<AttemptJournalError>,
270{
271 let plan = read_operation_plan(layout, expected_plan)?;
272 let authorities = plan.attempt_authorities()?;
273 let journals = read_original_journals(layout, &authorities, current)?;
274 Ok((plan, journals))
275}
276
277fn journal_path(layout: &BackupLayoutGuard, authority: &AttemptAuthorityRecord) -> PathBuf {
278 layout.root().join(format!(
279 "attempt-{}.json",
280 authority.binding().operation_sequence()
281 ))
282}
283
284#[derive(Debug, Error)]
286pub enum AttemptJournalError {
287 #[error("attempt journal authority mismatch")]
289 AuthorityMismatch,
290 #[error("attempt journal write outcome indeterminate; reopen retained evidence")]
292 IndeterminateWrite,
293 #[error(transparent)]
295 Record(#[from] AttemptJournalRecordError),
296 #[error(transparent)]
298 Lock(#[from] JournalLockError),
299 #[error(transparent)]
301 Persistence(#[from] PersistenceError),
302}
303
304#[cfg(all(test, unix))]
305mod tests;