1mod authority;
4mod history;
5pub use authority::{
6 AttemptAuthorityRecord, AttemptBudgetRecord, MAX_OPERATION_ATTEMPTS, OperationBindingRecord,
7 OperationBindingRequest,
8};
9
10use crate::model::{
11 artifacts::{ArtifactChecksumRecord, ChecksumError, canonical_hash},
12 bounded_sequence,
13};
14use history::{AttemptEventRecord, Projection};
15use serde::{Deserialize, Deserializer, Serialize};
16use thiserror::Error;
17
18pub const MAX_ATTEMPT_EVENTS: usize = 2048;
20pub const MAX_ATTEMPT_JOURNAL_BYTES: u64 = 1024 * 1024;
22
23#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
25#[serde(rename_all = "snake_case")]
26pub enum MutationOutcomeRecord {
27 Applied,
29 NotApplied,
31}
32
33#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
35#[serde(rename_all = "snake_case")]
36pub enum ObservationOutcomeRecord {
37 Applied,
39 NotApplied,
41 Uncertain,
43}
44
45#[derive(Clone, Debug)]
47pub struct MutationReceiptRequest {
48 pub attempt: u32,
50 pub request: String,
52 pub outcome: MutationOutcomeRecord,
54 pub evidence: String,
56}
57
58#[derive(Clone, Debug)]
64pub struct ObservationReceiptRequest {
65 pub attempt: u32,
67 pub request: String,
69 pub outcome: ObservationOutcomeRecord,
71 pub evidence: String,
73}
74
75#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
77pub struct AttemptJournalView {
78 pub authority: ArtifactChecksumRecord,
80 pub mutations_used: u32,
82 pub observations_used: u32,
84 pub mutations_remaining: u32,
86 pub observations_remaining: u32,
88 pub pending_mutation: Option<u32>,
90 pub pending_observation: Option<u32>,
92 pub applied: bool,
94 pub history_len: usize,
96}
97
98#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103pub enum AppliedReceiptEvidence<'a> {
104 Mutation {
106 attempt: u32,
108 evidence: &'a str,
110 },
111 Observation {
113 attempt: u32,
115 request: &'a str,
117 evidence: &'a str,
119 },
120}
121
122#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
124#[serde(try_from = "JournalFields")]
125pub struct AttemptJournalRecord {
126 version: u16,
127 authority: AttemptAuthorityRecord,
128 events: Vec<AttemptEventRecord>,
129 #[serde(skip)]
130 projection: Projection,
131}
132
133#[derive(Deserialize)]
134#[serde(deny_unknown_fields)]
135struct JournalFields {
136 version: u16,
137 authority: AttemptAuthorityRecord,
138 #[serde(deserialize_with = "bounded_events")]
139 events: Vec<AttemptEventRecord>,
140}
141
142impl TryFrom<JournalFields> for AttemptJournalRecord {
143 type Error = AttemptJournalRecordError;
144 fn try_from(mut fields: JournalFields) -> Result<Self, Self::Error> {
145 if fields.version != 1 {
146 return Err(AttemptJournalRecordError::UnsupportedVersion(
147 fields.version,
148 ));
149 }
150 let mut projection = Projection::empty();
151 for event in &mut fields.events {
152 event.normalize()?;
153 projection.apply(event, fields.authority.budget())?;
154 }
155 Ok(Self {
156 version: 1,
157 authority: fields.authority,
158 events: fields.events,
159 projection,
160 })
161 }
162}
163
164impl AttemptJournalRecord {
165 #[must_use]
167 pub const fn new(authority: AttemptAuthorityRecord) -> Self {
168 Self {
169 version: 1,
170 authority,
171 events: Vec::new(),
172 projection: Projection::empty(),
173 }
174 }
175 #[must_use]
177 pub const fn authority(&self) -> &AttemptAuthorityRecord {
178 &self.authority
179 }
180 #[must_use]
185 pub fn pending_observation_request(&self) -> Option<&str> {
186 self.projection
187 .pending_observation
188 .as_ref()
189 .map(|pending| pending.request.as_str())
190 }
191 #[must_use]
195 pub fn applied_receipt(&self) -> Option<AppliedReceiptEvidence<'_>> {
196 match self.events.last()? {
197 AttemptEventRecord::MutationResolved {
198 mutation,
199 outcome: MutationOutcomeRecord::Applied,
200 evidence,
201 } => Some(AppliedReceiptEvidence::Mutation {
202 attempt: *mutation,
203 evidence,
204 }),
205 AttemptEventRecord::ObservationRecorded {
206 observation,
207 request,
208 outcome: ObservationOutcomeRecord::Applied,
209 evidence,
210 } => Some(AppliedReceiptEvidence::Observation {
211 attempt: *observation,
212 request,
213 evidence,
214 }),
215 _ => None,
216 }
217 }
218 #[must_use]
223 pub fn view(&self) -> AttemptJournalView {
224 AttemptJournalView {
225 authority: self.authority.digest(),
226 mutations_used: self.projection.mutations_used,
227 observations_used: self.projection.observations_used,
228 mutations_remaining: self.authority.budget().mutations()
229 - self.projection.mutations_used,
230 observations_remaining: self.authority.budget().observations()
231 - self.projection.observations_used,
232 pending_mutation: self.projection.pending_mutation,
233 pending_observation: self
234 .projection
235 .pending_observation
236 .as_ref()
237 .map(|pending| pending.attempt),
238 applied: self.projection.applied,
239 history_len: self.events.len(),
240 }
241 }
242 #[must_use]
248 pub fn digest(&self) -> ArtifactChecksumRecord {
249 let mut bytes = b"ic-backup/attempt-journal-history/v1\0".to_vec();
250 bytes.extend_from_slice(self.authority.digest().hash().as_bytes());
251 bytes.extend_from_slice(&(self.events.len() as u64).to_be_bytes());
252 for event in &self.events {
253 event.append_digest_bytes(&mut bytes);
254 }
255 ArtifactChecksumRecord::from_bytes(&bytes)
256 }
257 pub(crate) fn reserve_mutation(&mut self) -> Result<u32, AttemptJournalRecordError> {
258 let attempt = self.projection.next_attempt;
259 self.append(AttemptEventRecord::MutationReserved { attempt })?;
260 Ok(attempt)
261 }
262 pub(crate) fn reserve_observation(
263 &mut self,
264 mutation: u32,
265 request: &str,
266 ) -> Result<u32, AttemptJournalRecordError> {
267 let attempt = self.projection.next_attempt;
268 self.append(AttemptEventRecord::ObservationReserved {
269 attempt,
270 mutation,
271 request: request.to_owned(),
272 })?;
273 Ok(attempt)
274 }
275 pub(crate) fn record_mutation(
276 &mut self,
277 receipt: MutationReceiptRequest,
278 ) -> Result<(), AttemptJournalRecordError> {
279 if canonical_hash(&receipt.request)? != self.authority.binding().request() {
280 return Err(AttemptJournalRecordError::RequestMismatch);
281 }
282 self.append(AttemptEventRecord::MutationResolved {
283 mutation: receipt.attempt,
284 outcome: receipt.outcome,
285 evidence: receipt.evidence,
286 })
287 }
288 pub(crate) fn record_observation(
289 &mut self,
290 receipt: ObservationReceiptRequest,
291 ) -> Result<(), AttemptJournalRecordError> {
292 self.append(AttemptEventRecord::ObservationRecorded {
293 observation: receipt.attempt,
294 request: receipt.request,
295 outcome: receipt.outcome,
296 evidence: receipt.evidence,
297 })
298 }
299 fn append(&mut self, mut event: AttemptEventRecord) -> Result<(), AttemptJournalRecordError> {
300 if self.events.len() == MAX_ATTEMPT_EVENTS {
301 return Err(AttemptJournalRecordError::HistoryTooLarge);
302 }
303 event.normalize()?;
304 let mut projection = self.projection.clone();
305 projection.apply(&event, self.authority.budget())?;
306 self.events.push(event);
307 self.projection = projection;
308 Ok(())
309 }
310}
311
312fn bounded_events<'de, D: Deserializer<'de>>(
313 deserializer: D,
314) -> Result<Vec<AttemptEventRecord>, D::Error> {
315 bounded_sequence::deserialize::<_, _, MAX_ATTEMPT_EVENTS>(
316 deserializer,
317 "a bounded chronological attempt event list",
318 AttemptJournalRecordError::HistoryTooLarge,
319 )
320}
321
322#[derive(Debug, Error)]
324pub enum AttemptJournalRecordError {
325 #[error("unsupported attempt journal version {0}")]
327 UnsupportedVersion(u16),
328 #[error("attempt allowance exceeds {MAX_OPERATION_ATTEMPTS}")]
330 BudgetTooLarge,
331 #[error("attempt history exceeds {MAX_ATTEMPT_EVENTS} events")]
333 HistoryTooLarge,
334 #[error("invalid attempt operation principal")]
336 InvalidPrincipal,
337 #[error(transparent)]
339 Checksum(#[from] ChecksumError),
340 #[error("mutation attempt {attempt} remains unresolved")]
342 MutationPending {
343 attempt: u32,
345 },
346 #[error("observation attempt {attempt} remains unresolved")]
348 ObservationPending {
349 attempt: u32,
351 },
352 #[error("no pending mutation attempt")]
354 NoPendingMutation,
355 #[error("no pending observation attempt")]
357 NoPendingObservation,
358 #[error("attempt identity mismatch: expected {expected}, actual {actual}")]
360 AttemptMismatch {
361 expected: u32,
363 actual: u32,
365 },
366 #[error("attempt request digest mismatch")]
368 RequestMismatch,
369 #[error("mutation attempt allowance exhausted")]
371 MutationBudgetExhausted,
372 #[error("observation attempt allowance exhausted")]
374 ObservationBudgetExhausted,
375 #[error("operation application already retained")]
377 AlreadyApplied,
378}
379
380#[cfg(test)]
381mod tests;