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
83 fn into_record(self) -> Result<AttemptJournalRecord, AttemptJournalError> {
84 self.check_usable()?;
85 Ok(self.record)
86 }
87 #[must_use]
89 pub fn path(&self) -> PathBuf {
90 journal_path(self.layout, self.record.authority())
91 }
92 pub fn reserve_mutation(&mut self) -> Result<u32, AttemptJournalError> {
97 self.reserve_with(AttemptJournalRecord::reserve_mutation, write_encoded)
98 }
99 pub fn reserve_planned_mutation(
110 &mut self,
111 expected_plan: &ArtifactChecksumRecord,
112 ) -> Result<u32, ExecutionProgressPersistenceError> {
113 let view = super::execution_progress::read_with_current(
114 self.layout,
115 expected_plan,
116 Some(self.record()?),
117 )?;
118 let sequence = self.record.authority().binding().operation_sequence();
119 if view.operations.iter().any(|operation| {
120 operation.operation_sequence == sequence
121 && operation.state
122 == crate::policy::execution_progress::OperationProgressState::AwaitingDependencies
123 }) {
124 return Err(ExecutionProgressPersistenceError::DependenciesUnapplied(sequence));
125 }
126 Ok(self.reserve_mutation()?)
127 }
128 pub fn reserve_observation(
134 &mut self,
135 mutation: u32,
136 request: &str,
137 ) -> Result<u32, AttemptJournalError> {
138 self.reserve_with(
139 |record| record.reserve_observation(mutation, request),
140 write_encoded,
141 )
142 }
143 pub fn reserve_planned_observation(
153 &mut self,
154 expected_plan: &ArtifactChecksumRecord,
155 mutation: u32,
156 request: &str,
157 ) -> Result<u32, ExecutionProgressPersistenceError> {
158 self.admit_planned_progress(expected_plan)?;
159 Ok(self.reserve_observation(mutation, request)?)
160 }
161 pub fn record_mutation(
166 &mut self,
167 receipt: MutationReceiptRequest,
168 ) -> Result<(), AttemptJournalError> {
169 self.reserve_with(|record| record.record_mutation(receipt), write_encoded)
170 }
171 pub fn record_observation(
179 &mut self,
180 receipt: ObservationReceiptRequest,
181 ) -> Result<(), AttemptJournalError> {
182 self.reserve_with(|record| record.record_observation(receipt), write_encoded)
183 }
184
185 pub fn record_planned_observation(
194 &mut self,
195 expected_plan: &ArtifactChecksumRecord,
196 receipt: ObservationReceiptRequest,
197 ) -> Result<(), ExecutionProgressPersistenceError> {
198 self.admit_planned_progress(expected_plan)?;
199 Ok(self.record_observation(receipt)?)
200 }
201
202 pub(crate) fn admit_planned_progress(
203 &self,
204 expected_plan: &ArtifactChecksumRecord,
205 ) -> Result<(), ExecutionProgressPersistenceError> {
206 super::execution_progress::read_with_current(
207 self.layout,
208 expected_plan,
209 Some(self.record()?),
210 )?;
211 Ok(())
212 }
213
214 fn reserve_with<T>(
215 &mut self,
216 transition: impl FnOnce(&mut AttemptJournalRecord) -> Result<T, AttemptJournalRecordError>,
217 write: impl FnOnce(&Path, &[u8]) -> Result<(), PersistenceError>,
218 ) -> Result<T, AttemptJournalError> {
219 self.check_usable()?;
220 let mut next = self.record.clone();
221 let result = transition(&mut next)?;
222 let bytes = encode_json(&next, MAX_ATTEMPT_JOURNAL_BYTES)?;
223 let path = self.path();
224 super::json::replace_cached_record(&mut self.record, &mut self.usable, next, || {
225 write(&path, &bytes)
226 })?;
227 Ok(result)
228 }
229 fn check_usable(&self) -> Result<(), AttemptJournalError> {
230 if !self.usable {
231 return Err(AttemptJournalError::IndeterminateWrite);
232 }
233 self.layout.check_root()?;
234 Ok(())
235 }
236}
237
238pub(super) fn read_original_journals(
239 layout: &BackupLayoutGuard,
240 authorities: &[AttemptAuthorityRecord],
241 current: Option<&AttemptJournalRecord>,
242) -> Result<Vec<AttemptJournalRecord>, AttemptJournalError> {
243 layout.check_root()?;
244 if let Some(current) = current
245 && !authorities.contains(current.authority())
246 {
247 return Err(AttemptJournalError::AuthorityMismatch);
248 }
249 let mut journals = Vec::with_capacity(authorities.len());
250 for authority in authorities {
251 if let Some(current) = current
252 && current.authority() == authority
253 {
254 journals.push(current.clone());
255 } else {
256 let guard = AttemptJournalGuard::open(layout, authority)?;
257 journals.push(guard.into_record()?);
258 }
259 }
260 layout.check_root()?;
261 Ok(journals)
262}
263
264pub(super) fn read_original_plan_journals<E>(
265 layout: &BackupLayoutGuard,
266 expected_plan: &ArtifactChecksumRecord,
267 current: Option<&AttemptJournalRecord>,
268) -> Result<(OperationPlanRecord, Vec<AttemptJournalRecord>), E>
269where
270 E: From<OperationPlanPersistenceError> + From<OperationPlanError> + From<AttemptJournalError>,
271{
272 let plan = read_operation_plan(layout, expected_plan)?;
273 let authorities = plan.attempt_authorities()?;
274 let journals = read_original_journals(layout, &authorities, current)?;
275 Ok((plan, journals))
276}
277
278fn journal_path(layout: &BackupLayoutGuard, authority: &AttemptAuthorityRecord) -> PathBuf {
279 layout.root().join(format!(
280 "attempt-{}.json",
281 authority.binding().operation_sequence()
282 ))
283}
284
285#[derive(Debug, Error)]
287pub enum AttemptJournalError {
288 #[error("attempt journal authority mismatch")]
290 AuthorityMismatch,
291 #[error("attempt journal write outcome indeterminate; reopen retained evidence")]
293 IndeterminateWrite,
294 #[error(transparent)]
296 Record(#[from] AttemptJournalRecordError),
297 #[error(transparent)]
299 Lock(#[from] JournalLockError),
300 #[error(transparent)]
302 Persistence(#[from] PersistenceError),
303}
304
305#[cfg(all(test, unix))]
306mod tests;