1mod authority;
4mod history;
5pub use authority::{
6 AttemptAuthorityRecord, AttemptBudgetRecord, MAX_OPERATION_ATTEMPTS, OperationBindingRecord,
7 OperationBindingRequest,
8};
9
10use crate::model::artifacts::{ArtifactChecksumRecord, ChecksumError, canonical_hash};
11use history::{AttemptEventRecord, Projection};
12use serde::{Deserialize, Deserializer, Serialize, de};
13use std::fmt;
14use thiserror::Error;
15
16pub const MAX_ATTEMPT_EVENTS: usize = 2048;
18pub const MAX_ATTEMPT_JOURNAL_BYTES: u64 = 1024 * 1024;
20
21#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
23#[serde(rename_all = "snake_case")]
24pub enum MutationOutcomeRecord {
25 Applied,
27 NotApplied,
29}
30
31#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
33#[serde(rename_all = "snake_case")]
34pub enum ObservationOutcomeRecord {
35 Applied,
37 NotApplied,
39 Uncertain,
41}
42
43#[derive(Clone, Debug)]
45pub struct MutationReceiptRequest {
46 pub attempt: u32,
48 pub request: String,
50 pub outcome: MutationOutcomeRecord,
52 pub evidence: String,
54}
55
56#[derive(Clone, Debug)]
62pub struct ObservationReceiptRequest {
63 pub attempt: u32,
65 pub request: String,
67 pub outcome: ObservationOutcomeRecord,
69 pub evidence: String,
71}
72
73#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
75pub struct AttemptJournalView {
76 pub authority: ArtifactChecksumRecord,
78 pub mutations_used: u32,
80 pub observations_used: u32,
82 pub mutations_remaining: u32,
84 pub observations_remaining: u32,
86 pub pending_mutation: Option<u32>,
88 pub pending_observation: Option<u32>,
90 pub applied: bool,
92 pub history_len: usize,
94}
95
96#[derive(Clone, Copy, Debug, Eq, PartialEq)]
101pub enum AppliedReceiptEvidence<'a> {
102 Mutation {
104 attempt: u32,
106 evidence: &'a str,
108 },
109 Observation {
111 attempt: u32,
113 request: &'a str,
115 evidence: &'a str,
117 },
118}
119
120#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
122#[serde(try_from = "JournalFields")]
123pub struct AttemptJournalRecord {
124 version: u16,
125 authority: AttemptAuthorityRecord,
126 events: Vec<AttemptEventRecord>,
127 #[serde(skip)]
128 projection: Projection,
129}
130
131#[derive(Deserialize)]
132#[serde(deny_unknown_fields)]
133struct JournalFields {
134 version: u16,
135 authority: AttemptAuthorityRecord,
136 #[serde(deserialize_with = "bounded_events")]
137 events: Vec<AttemptEventRecord>,
138}
139
140impl TryFrom<JournalFields> for AttemptJournalRecord {
141 type Error = AttemptJournalRecordError;
142 fn try_from(mut fields: JournalFields) -> Result<Self, Self::Error> {
143 if fields.version != 1 {
144 return Err(AttemptJournalRecordError::UnsupportedVersion(
145 fields.version,
146 ));
147 }
148 let mut projection = Projection::empty();
149 for event in &mut fields.events {
150 event.normalize()?;
151 projection.apply(event, fields.authority.budget())?;
152 }
153 Ok(Self {
154 version: 1,
155 authority: fields.authority,
156 events: fields.events,
157 projection,
158 })
159 }
160}
161
162impl AttemptJournalRecord {
163 #[must_use]
165 pub const fn new(authority: AttemptAuthorityRecord) -> Self {
166 Self {
167 version: 1,
168 authority,
169 events: Vec::new(),
170 projection: Projection::empty(),
171 }
172 }
173 #[must_use]
175 pub const fn authority(&self) -> &AttemptAuthorityRecord {
176 &self.authority
177 }
178 #[must_use]
183 pub fn pending_observation_request(&self) -> Option<&str> {
184 self.projection
185 .pending_observation
186 .as_ref()
187 .map(|pending| pending.request.as_str())
188 }
189 #[must_use]
193 pub fn applied_receipt(&self) -> Option<AppliedReceiptEvidence<'_>> {
194 match self.events.last()? {
195 AttemptEventRecord::MutationResolved {
196 mutation,
197 outcome: MutationOutcomeRecord::Applied,
198 evidence,
199 } => Some(AppliedReceiptEvidence::Mutation {
200 attempt: *mutation,
201 evidence,
202 }),
203 AttemptEventRecord::ObservationRecorded {
204 observation,
205 request,
206 outcome: ObservationOutcomeRecord::Applied,
207 evidence,
208 } => Some(AppliedReceiptEvidence::Observation {
209 attempt: *observation,
210 request,
211 evidence,
212 }),
213 _ => None,
214 }
215 }
216 #[must_use]
221 pub fn view(&self) -> AttemptJournalView {
222 AttemptJournalView {
223 authority: self.authority.digest(),
224 mutations_used: self.projection.mutations_used,
225 observations_used: self.projection.observations_used,
226 mutations_remaining: self.authority.budget().mutations()
227 - self.projection.mutations_used,
228 observations_remaining: self.authority.budget().observations()
229 - self.projection.observations_used,
230 pending_mutation: self.projection.pending_mutation,
231 pending_observation: self
232 .projection
233 .pending_observation
234 .as_ref()
235 .map(|pending| pending.attempt),
236 applied: self.projection.applied,
237 history_len: self.events.len(),
238 }
239 }
240 #[must_use]
246 pub fn digest(&self) -> ArtifactChecksumRecord {
247 let mut bytes = b"ic-backup/attempt-journal-history/v1\0".to_vec();
248 bytes.extend_from_slice(self.authority.digest().hash().as_bytes());
249 bytes.extend_from_slice(&(self.events.len() as u64).to_be_bytes());
250 for event in &self.events {
251 event.append_digest_bytes(&mut bytes);
252 }
253 ArtifactChecksumRecord::from_bytes(&bytes)
254 }
255 pub(crate) fn reserve_mutation(&mut self) -> Result<u32, AttemptJournalRecordError> {
256 let attempt = self.projection.next_attempt;
257 self.append(AttemptEventRecord::MutationReserved { attempt })?;
258 Ok(attempt)
259 }
260 pub(crate) fn reserve_observation(
261 &mut self,
262 mutation: u32,
263 request: &str,
264 ) -> Result<u32, AttemptJournalRecordError> {
265 let attempt = self.projection.next_attempt;
266 self.append(AttemptEventRecord::ObservationReserved {
267 attempt,
268 mutation,
269 request: request.to_owned(),
270 })?;
271 Ok(attempt)
272 }
273 pub(crate) fn record_mutation(
274 &mut self,
275 receipt: MutationReceiptRequest,
276 ) -> Result<(), AttemptJournalRecordError> {
277 if canonical_hash(&receipt.request)? != self.authority.binding().request() {
278 return Err(AttemptJournalRecordError::RequestMismatch);
279 }
280 self.append(AttemptEventRecord::MutationResolved {
281 mutation: receipt.attempt,
282 outcome: receipt.outcome,
283 evidence: receipt.evidence,
284 })
285 }
286 pub(crate) fn record_observation(
287 &mut self,
288 receipt: ObservationReceiptRequest,
289 ) -> Result<(), AttemptJournalRecordError> {
290 self.append(AttemptEventRecord::ObservationRecorded {
291 observation: receipt.attempt,
292 request: receipt.request,
293 outcome: receipt.outcome,
294 evidence: receipt.evidence,
295 })
296 }
297 fn append(&mut self, mut event: AttemptEventRecord) -> Result<(), AttemptJournalRecordError> {
298 if self.events.len() == MAX_ATTEMPT_EVENTS {
299 return Err(AttemptJournalRecordError::HistoryTooLarge);
300 }
301 event.normalize()?;
302 let mut projection = self.projection.clone();
303 projection.apply(&event, self.authority.budget())?;
304 self.events.push(event);
305 self.projection = projection;
306 Ok(())
307 }
308}
309
310fn bounded_events<'de, D: Deserializer<'de>>(
311 deserializer: D,
312) -> Result<Vec<AttemptEventRecord>, D::Error> {
313 struct EventsVisitor;
314 impl<'de> de::Visitor<'de> for EventsVisitor {
315 type Value = Vec<AttemptEventRecord>;
316 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
317 f.write_str("a bounded chronological attempt event list")
318 }
319 fn visit_seq<A: de::SeqAccess<'de>>(
320 self,
321 mut sequence: A,
322 ) -> Result<Self::Value, A::Error> {
323 let mut events = Vec::new();
324 while events.len() < MAX_ATTEMPT_EVENTS {
325 match sequence.next_element()? {
326 Some(event) => events.push(event),
327 None => return Ok(events),
328 }
329 }
330 if sequence.next_element::<de::IgnoredAny>()?.is_some() {
331 return Err(de::Error::custom(
332 AttemptJournalRecordError::HistoryTooLarge,
333 ));
334 }
335 Ok(events)
336 }
337 }
338 deserializer.deserialize_seq(EventsVisitor)
339}
340
341#[derive(Debug, Error)]
343pub enum AttemptJournalRecordError {
344 #[error("unsupported attempt journal version {0}")]
346 UnsupportedVersion(u16),
347 #[error("attempt allowance exceeds {MAX_OPERATION_ATTEMPTS}")]
349 BudgetTooLarge,
350 #[error("attempt history exceeds {MAX_ATTEMPT_EVENTS} events")]
352 HistoryTooLarge,
353 #[error("invalid attempt operation principal")]
355 InvalidPrincipal,
356 #[error(transparent)]
358 Checksum(#[from] ChecksumError),
359 #[error("mutation attempt {attempt} remains unresolved")]
361 MutationPending {
362 attempt: u32,
364 },
365 #[error("observation attempt {attempt} remains unresolved")]
367 ObservationPending {
368 attempt: u32,
370 },
371 #[error("no pending mutation attempt")]
373 NoPendingMutation,
374 #[error("no pending observation attempt")]
376 NoPendingObservation,
377 #[error("attempt identity mismatch: expected {expected}, actual {actual}")]
379 AttemptMismatch {
380 expected: u32,
382 actual: u32,
384 },
385 #[error("attempt request digest mismatch")]
387 RequestMismatch,
388 #[error("mutation attempt allowance exhausted")]
390 MutationBudgetExhausted,
391 #[error("observation attempt allowance exhausted")]
393 ObservationBudgetExhausted,
394 #[error("operation application already retained")]
396 AlreadyApplied,
397}
398
399#[cfg(test)]
400mod tests;