ic_backup/model/attempt_journal/
mod.rs1mod authority;
4mod history;
5pub use authority::{
6 AttemptAuthorityRecord, AttemptBudgetRecord, MAX_OPERATION_ATTEMPTS, OperationBindingRecord,
7 OperationBindingRequest,
8};
9
10use crate::model::artifacts::{ArtifactChecksumRecord, ChecksumError};
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, Debug, Deserialize, Eq, PartialEq, Serialize)]
98#[serde(try_from = "JournalFields")]
99pub struct AttemptJournalRecord {
100 version: u16,
101 authority: AttemptAuthorityRecord,
102 events: Vec<AttemptEventRecord>,
103 #[serde(skip)]
104 projection: Projection,
105}
106
107#[derive(Deserialize)]
108#[serde(deny_unknown_fields)]
109struct JournalFields {
110 version: u16,
111 authority: AttemptAuthorityRecord,
112 #[serde(deserialize_with = "bounded_events")]
113 events: Vec<AttemptEventRecord>,
114}
115
116impl TryFrom<JournalFields> for AttemptJournalRecord {
117 type Error = AttemptJournalRecordError;
118 fn try_from(mut fields: JournalFields) -> Result<Self, Self::Error> {
119 if fields.version != 1 {
120 return Err(AttemptJournalRecordError::UnsupportedVersion(
121 fields.version,
122 ));
123 }
124 let mut projection = Projection::empty();
125 for event in &mut fields.events {
126 event.normalize()?;
127 projection.apply(event, fields.authority.budget())?;
128 }
129 Ok(Self {
130 version: 1,
131 authority: fields.authority,
132 events: fields.events,
133 projection,
134 })
135 }
136}
137
138impl AttemptJournalRecord {
139 #[must_use]
141 pub const fn new(authority: AttemptAuthorityRecord) -> Self {
142 Self {
143 version: 1,
144 authority,
145 events: Vec::new(),
146 projection: Projection::empty(),
147 }
148 }
149 #[must_use]
151 pub const fn authority(&self) -> &AttemptAuthorityRecord {
152 &self.authority
153 }
154 #[must_use]
156 pub fn view(&self) -> AttemptJournalView {
157 AttemptJournalView {
158 authority: self.authority.digest(),
159 mutations_used: self.projection.mutations_used,
160 observations_used: self.projection.observations_used,
161 mutations_remaining: self.authority.budget().mutations()
162 - self.projection.mutations_used,
163 observations_remaining: self.authority.budget().observations()
164 - self.projection.observations_used,
165 pending_mutation: self.projection.pending_mutation,
166 pending_observation: self
167 .projection
168 .pending_observation
169 .as_ref()
170 .map(|pending| pending.attempt),
171 applied: self.projection.applied,
172 history_len: self.events.len(),
173 }
174 }
175 pub(crate) fn reserve_mutation(&mut self) -> Result<u32, AttemptJournalRecordError> {
176 let attempt = self.projection.next_attempt;
177 self.append(AttemptEventRecord::MutationReserved { attempt })?;
178 Ok(attempt)
179 }
180 pub(crate) fn reserve_observation(
181 &mut self,
182 mutation: u32,
183 request: &str,
184 ) -> Result<u32, AttemptJournalRecordError> {
185 let attempt = self.projection.next_attempt;
186 self.append(AttemptEventRecord::ObservationReserved {
187 attempt,
188 mutation,
189 request: request.to_owned(),
190 })?;
191 Ok(attempt)
192 }
193 pub(crate) fn record_mutation(
194 &mut self,
195 receipt: MutationReceiptRequest,
196 ) -> Result<(), AttemptJournalRecordError> {
197 if canonical_hash(&receipt.request)? != self.authority.binding().request() {
198 return Err(AttemptJournalRecordError::RequestMismatch);
199 }
200 self.append(AttemptEventRecord::MutationResolved {
201 mutation: receipt.attempt,
202 outcome: receipt.outcome,
203 evidence: receipt.evidence,
204 })
205 }
206 pub(crate) fn record_observation(
207 &mut self,
208 receipt: ObservationReceiptRequest,
209 ) -> Result<(), AttemptJournalRecordError> {
210 self.append(AttemptEventRecord::ObservationRecorded {
211 observation: receipt.attempt,
212 request: receipt.request,
213 outcome: receipt.outcome,
214 evidence: receipt.evidence,
215 })
216 }
217 fn append(&mut self, mut event: AttemptEventRecord) -> Result<(), AttemptJournalRecordError> {
218 if self.events.len() == MAX_ATTEMPT_EVENTS {
219 return Err(AttemptJournalRecordError::HistoryTooLarge);
220 }
221 event.normalize()?;
222 let mut projection = self.projection.clone();
223 projection.apply(&event, self.authority.budget())?;
224 self.events.push(event);
225 self.projection = projection;
226 Ok(())
227 }
228}
229
230fn canonical_hash(hash: &str) -> Result<String, AttemptJournalRecordError> {
231 Ok(ArtifactChecksumRecord::from_hash(hash)?.hash().to_owned())
232}
233
234fn bounded_events<'de, D: Deserializer<'de>>(
235 deserializer: D,
236) -> Result<Vec<AttemptEventRecord>, D::Error> {
237 struct EventsVisitor;
238 impl<'de> de::Visitor<'de> for EventsVisitor {
239 type Value = Vec<AttemptEventRecord>;
240 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241 f.write_str("a bounded chronological attempt event list")
242 }
243 fn visit_seq<A: de::SeqAccess<'de>>(
244 self,
245 mut sequence: A,
246 ) -> Result<Self::Value, A::Error> {
247 let mut events = Vec::new();
248 while events.len() < MAX_ATTEMPT_EVENTS {
249 match sequence.next_element()? {
250 Some(event) => events.push(event),
251 None => return Ok(events),
252 }
253 }
254 if sequence.next_element::<de::IgnoredAny>()?.is_some() {
255 return Err(de::Error::custom(
256 AttemptJournalRecordError::HistoryTooLarge,
257 ));
258 }
259 Ok(events)
260 }
261 }
262 deserializer.deserialize_seq(EventsVisitor)
263}
264
265#[derive(Debug, Error)]
267pub enum AttemptJournalRecordError {
268 #[error("unsupported attempt journal version {0}")]
270 UnsupportedVersion(u16),
271 #[error("attempt allowance exceeds {MAX_OPERATION_ATTEMPTS}")]
273 BudgetTooLarge,
274 #[error("attempt history exceeds {MAX_ATTEMPT_EVENTS} events")]
276 HistoryTooLarge,
277 #[error("invalid attempt operation principal")]
279 InvalidPrincipal,
280 #[error(transparent)]
282 Checksum(#[from] ChecksumError),
283 #[error("mutation attempt {attempt} remains unresolved")]
285 MutationPending {
286 attempt: u32,
288 },
289 #[error("observation attempt {attempt} remains unresolved")]
291 ObservationPending {
292 attempt: u32,
294 },
295 #[error("no pending mutation attempt")]
297 NoPendingMutation,
298 #[error("no pending observation attempt")]
300 NoPendingObservation,
301 #[error("attempt identity mismatch: expected {expected}, actual {actual}")]
303 AttemptMismatch {
304 expected: u32,
306 actual: u32,
308 },
309 #[error("attempt request digest mismatch")]
311 RequestMismatch,
312 #[error("mutation attempt allowance exhausted")]
314 MutationBudgetExhausted,
315 #[error("observation attempt allowance exhausted")]
317 ObservationBudgetExhausted,
318 #[error("operation application already retained")]
320 AlreadyApplied,
321}
322
323#[cfg(test)]
324mod tests;