1use std::collections::BTreeMap;
17use std::error::Error;
18use std::fmt;
19use std::sync::{Arc, Mutex, PoisonError};
20
21use serde_json::{Map, Number, Value};
22use sha2::{Digest, Sha256};
23
24use crate::{
25 BackoffSleeper, BoxFuture, ChunkDeliveryMode, ChunkTransactionContext, ClassifierRevision,
26 FailureCategory, FaultPhase, FaultPolicy, RetryLimit, RetryOrdinal, RetryStateLimit,
27 SkipCounts, StepName,
28};
29
30const RETRY_KEY_DOMAIN: &[u8] = b"oxide-batch/retry-key/1";
32
33#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
40pub struct RetryKey([u8; 32]);
41
42impl RetryKey {
43 #[must_use]
45 pub(crate) fn derive(
46 definition_digest: &[u8; 32],
47 step_name: &StepName,
48 phase: FaultPhase,
49 checkpoint_digest: &[u8; 32],
50 ordinal: u64,
51 ) -> Self {
52 let mut hasher = Sha256::new();
53 hasher.update(RETRY_KEY_DOMAIN);
54 hasher.update(definition_digest);
55 hasher.update((step_name.as_str().len() as u64).to_be_bytes());
56 hasher.update(step_name.as_str().as_bytes());
57 hasher.update(phase.as_str().as_bytes());
58 hasher.update([0]);
59 hasher.update(checkpoint_digest);
60 hasher.update(ordinal.to_be_bytes());
61 Self(hasher.finalize().into())
62 }
63
64 #[must_use]
69 pub const fn from_bytes(digest: [u8; 32]) -> Self {
70 Self(digest)
71 }
72
73 #[must_use]
78 pub const fn as_bytes(&self) -> &[u8; 32] {
79 &self.0
80 }
81}
82
83impl fmt::Debug for RetryKey {
84 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
85 formatter
86 .debug_struct("RetryKey")
87 .field("digest", &"<redacted>")
88 .finish()
89 }
90}
91
92#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
97pub struct RetryReservation {
98 key: RetryKey,
99 phase: FaultPhase,
100 category: FailureCategory,
101 ordinal: RetryOrdinal,
102}
103
104impl RetryReservation {
105 #[must_use]
107 pub const fn new(
108 key: RetryKey,
109 phase: FaultPhase,
110 category: FailureCategory,
111 ordinal: RetryOrdinal,
112 ) -> Self {
113 Self {
114 key,
115 phase,
116 category,
117 ordinal,
118 }
119 }
120
121 #[must_use]
123 pub const fn key(self) -> RetryKey {
124 self.key
125 }
126
127 #[must_use]
129 pub const fn phase(self) -> FaultPhase {
130 self.phase
131 }
132
133 #[must_use]
135 pub const fn category(self) -> FailureCategory {
136 self.category
137 }
138
139 #[must_use]
141 pub const fn ordinal(self) -> RetryOrdinal {
142 self.ordinal
143 }
144}
145
146#[derive(Clone, Copy, Debug, Eq, PartialEq)]
148#[non_exhaustive]
149pub enum FaultStateError {
150 CapacityExhausted {
152 max: u32,
154 },
155 StaleReservation,
160 Corrupt(FaultStateFormatError),
162 Unbound,
164 Unavailable,
166}
167
168impl fmt::Display for FaultStateError {
169 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
170 match self {
171 Self::CapacityExhausted { max } => {
172 write!(
173 formatter,
174 "step already retains {max} unresolved retry keys"
175 )
176 }
177 Self::StaleReservation => {
178 formatter.write_str("retry reservation lost to a newer persisted ordinal")
179 }
180 Self::Corrupt(error) => write!(formatter, "durable fault state is unusable: {error}"),
181 Self::Unbound => formatter.write_str("durable fault state has no bound step execution"),
182 Self::Unavailable => formatter.write_str("fault state is unavailable"),
183 }
184 }
185}
186
187impl Error for FaultStateError {
188 fn source(&self) -> Option<&(dyn Error + 'static)> {
189 match self {
190 Self::Corrupt(error) => Some(error),
191 _ => None,
192 }
193 }
194}
195
196impl From<FaultStateFormatError> for FaultStateError {
197 fn from(error: FaultStateFormatError) -> Self {
198 Self::Corrupt(error)
199 }
200}
201
202#[derive(Clone, Debug, Eq, PartialEq)]
207pub struct FaultStateEntry {
208 key: RetryKey,
209 phase: FaultPhase,
210 category: FailureCategory,
211 ordinal: RetryOrdinal,
212 revision: ClassifierRevision,
213}
214
215impl FaultStateEntry {
216 #[must_use]
218 pub const fn new(
219 key: RetryKey,
220 phase: FaultPhase,
221 category: FailureCategory,
222 ordinal: RetryOrdinal,
223 revision: ClassifierRevision,
224 ) -> Self {
225 Self {
226 key,
227 phase,
228 category,
229 ordinal,
230 revision,
231 }
232 }
233
234 #[must_use]
236 pub const fn key(&self) -> RetryKey {
237 self.key
238 }
239
240 #[must_use]
242 pub const fn phase(&self) -> FaultPhase {
243 self.phase
244 }
245
246 #[must_use]
248 pub const fn category(&self) -> FailureCategory {
249 self.category
250 }
251
252 #[must_use]
254 pub const fn ordinal(&self) -> RetryOrdinal {
255 self.ordinal
256 }
257
258 #[must_use]
260 pub const fn revision(&self) -> &ClassifierRevision {
261 &self.revision
262 }
263
264 fn to_json(&self) -> Value {
265 let mut object = Map::new();
266 object.insert(
267 String::from("category"),
268 Value::String(String::from(self.category.durable_code())),
269 );
270 object.insert(String::from("key"), Value::String(hex(self.key.as_bytes())));
271 object.insert(
272 String::from("ordinal"),
273 Value::Number(Number::from(self.ordinal.get())),
274 );
275 object.insert(
276 String::from("phase"),
277 Value::String(String::from(self.phase.as_str())),
278 );
279 object.insert(
280 String::from("revision"),
281 Value::String(String::from(self.revision.as_str())),
282 );
283 Value::Object(object)
284 }
285
286 fn from_json(value: &Value) -> Result<Self, FaultStateFormatError> {
287 let object = value
288 .as_object()
289 .ok_or(FaultStateFormatError::MalformedEntry)?;
290 let key = object
291 .get("key")
292 .and_then(Value::as_str)
293 .and_then(unhex)
294 .map(RetryKey::from_bytes)
295 .ok_or(FaultStateFormatError::MalformedEntry)?;
296 let phase = object
297 .get("phase")
298 .and_then(Value::as_str)
299 .and_then(FaultPhase::from_durable_name)
300 .ok_or(FaultStateFormatError::UnknownEnumeration)?;
301 let category = object
302 .get("category")
303 .and_then(Value::as_str)
304 .and_then(FailureCategory::from_durable_code)
305 .ok_or(FaultStateFormatError::UnknownEnumeration)?;
306 let ordinal = object
307 .get("ordinal")
308 .and_then(Value::as_u64)
309 .and_then(|value| u32::try_from(value).ok())
310 .and_then(|value| RetryOrdinal::new(value).ok())
311 .ok_or(FaultStateFormatError::MalformedEntry)?;
312 let revision = object
313 .get("revision")
314 .and_then(Value::as_str)
315 .and_then(|value| ClassifierRevision::new(value).ok())
316 .ok_or(FaultStateFormatError::MalformedEntry)?;
317 Ok(Self::new(key, phase, category, ordinal, revision))
318 }
319}
320
321#[derive(Clone, Debug, Eq, PartialEq)]
328pub struct FaultStateEnvelope {
329 checkpoint_digest: [u8; 32],
330 entries: Vec<FaultStateEntry>,
331}
332
333impl FaultStateEnvelope {
334 pub const FORMAT: &'static str = "oxide-batch.fault-state";
336 pub const FORMAT_VERSION: u16 = 1;
338 pub const SCHEMA_VERSION: u32 = 1;
340 pub const MAX_BYTES: usize = 64 * 1024;
342 pub const MAX_ENTRIES: usize = 256;
344
345 #[must_use]
347 pub const fn empty() -> Self {
348 Self {
349 checkpoint_digest: [0; 32],
350 entries: Vec::new(),
351 }
352 }
353
354 pub fn new(
363 checkpoint_digest: [u8; 32],
364 entries: impl IntoIterator<Item = FaultStateEntry>,
365 ) -> Result<Self, FaultStateFormatError> {
366 let mut entries: Vec<FaultStateEntry> = entries.into_iter().collect();
367 if entries.len() > Self::MAX_ENTRIES {
368 return Err(FaultStateFormatError::TooManyEntries {
369 max: Self::MAX_ENTRIES,
370 });
371 }
372 entries.sort_by_key(FaultStateEntry::key);
373 if entries.windows(2).any(|pair| pair[0].key == pair[1].key) {
374 return Err(FaultStateFormatError::DuplicateKey);
375 }
376 if entries.is_empty() {
377 if checkpoint_digest != [0; 32] {
378 return Err(FaultStateFormatError::CheckpointMismatch);
379 }
380 } else if checkpoint_digest == [0; 32] {
381 return Err(FaultStateFormatError::CheckpointMismatch);
382 }
383 Ok(Self {
384 checkpoint_digest,
385 entries,
386 })
387 }
388
389 #[must_use]
391 pub const fn checkpoint_digest(&self) -> &[u8; 32] {
392 &self.checkpoint_digest
393 }
394
395 #[must_use]
397 pub fn entries(&self) -> &[FaultStateEntry] {
398 &self.entries
399 }
400
401 #[must_use]
403 pub fn is_empty(&self) -> bool {
404 self.entries.is_empty()
405 }
406
407 #[must_use]
409 pub fn len(&self) -> usize {
410 self.entries.len()
411 }
412
413 #[must_use]
415 pub fn reserved_ordinal(&self, key: RetryKey) -> Option<RetryOrdinal> {
416 self.entry(key).map(FaultStateEntry::ordinal)
417 }
418
419 #[must_use]
421 pub fn entry(&self, key: RetryKey) -> Option<&FaultStateEntry> {
422 self.entries
423 .binary_search_by(|entry| entry.key.cmp(&key))
424 .ok()
425 .map(|index| &self.entries[index])
426 }
427
428 pub fn reserved(
440 &self,
441 entry: FaultStateEntry,
442 checkpoint_digest: [u8; 32],
443 limit: RetryStateLimit,
444 ) -> Result<Self, FaultStateError> {
445 if !self.entries.is_empty() && self.checkpoint_digest != checkpoint_digest {
446 return Err(FaultStateError::Corrupt(
447 FaultStateFormatError::CheckpointMismatch,
448 ));
449 }
450 let expected = self
451 .reserved_ordinal(entry.key())
452 .unwrap_or(RetryOrdinal::INITIAL)
453 .checked_next()
454 .map_err(|_| FaultStateError::StaleReservation)?;
455 if entry.ordinal() != expected {
456 return Err(FaultStateError::StaleReservation);
457 }
458 let mut entries = self.entries.clone();
459 match entries.binary_search_by(|existing| existing.key.cmp(&entry.key())) {
460 Ok(index) => entries[index] = entry,
461 Err(index) => {
462 if entries.len() >= limit.get() as usize {
463 return Err(FaultStateError::CapacityExhausted { max: limit.get() });
464 }
465 entries.insert(index, entry);
466 }
467 }
468 Ok(Self {
469 checkpoint_digest,
470 entries,
471 })
472 }
473
474 pub fn to_canonical_json(&self) -> Result<Vec<u8>, FaultStateFormatError> {
480 let mut object = Map::new();
481 object.insert(
482 String::from("checkpoint"),
483 Value::String(hex(&self.checkpoint_digest)),
484 );
485 object.insert(
486 String::from("entries"),
487 Value::Array(self.entries.iter().map(FaultStateEntry::to_json).collect()),
488 );
489 let bytes = serde_json::to_vec(&Value::Object(object))
490 .map_err(|_| FaultStateFormatError::Malformed)?;
491 if bytes.len() > Self::MAX_BYTES {
492 return Err(FaultStateFormatError::TooLarge {
493 max_bytes: Self::MAX_BYTES,
494 });
495 }
496 Ok(bytes)
497 }
498
499 pub fn checksum(&self) -> Result<[u8; 32], FaultStateFormatError> {
505 Ok(Sha256::digest(self.to_canonical_json()?).into())
506 }
507
508 pub fn from_canonical_json(
518 format_version: u16,
519 schema: &str,
520 schema_version: u32,
521 bytes: &[u8],
522 checksum: &[u8; 32],
523 ) -> Result<Self, FaultStateFormatError> {
524 if format_version != Self::FORMAT_VERSION || schema != Self::FORMAT {
525 return Err(FaultStateFormatError::UnsupportedFormat);
526 }
527 if schema_version != Self::SCHEMA_VERSION {
528 return Err(FaultStateFormatError::UnsupportedSchemaVersion);
529 }
530 if bytes.len() > Self::MAX_BYTES {
531 return Err(FaultStateFormatError::TooLarge {
532 max_bytes: Self::MAX_BYTES,
533 });
534 }
535 let value: Value =
536 serde_json::from_slice(bytes).map_err(|_| FaultStateFormatError::Malformed)?;
537 let object = value.as_object().ok_or(FaultStateFormatError::Malformed)?;
538 let checkpoint_digest = object
539 .get("checkpoint")
540 .and_then(Value::as_str)
541 .and_then(unhex)
542 .ok_or(FaultStateFormatError::Malformed)?;
543 let raw = object
544 .get("entries")
545 .and_then(Value::as_array)
546 .ok_or(FaultStateFormatError::Malformed)?;
547 if raw.len() > Self::MAX_ENTRIES {
548 return Err(FaultStateFormatError::TooManyEntries {
549 max: Self::MAX_ENTRIES,
550 });
551 }
552 let entries = raw
553 .iter()
554 .map(FaultStateEntry::from_json)
555 .collect::<Result<Vec<_>, _>>()?;
556 if entries.windows(2).any(|pair| pair[0].key >= pair[1].key) {
557 return Err(FaultStateFormatError::UnsortedEntries);
558 }
559 let envelope = Self::new(checkpoint_digest, entries)?;
560 if &envelope.checksum()? != checksum {
561 return Err(FaultStateFormatError::ChecksumMismatch);
562 }
563 Ok(envelope)
564 }
565
566 pub fn validate_for(
576 &self,
577 retry_limit: RetryLimit,
578 state_limit: RetryStateLimit,
579 checkpoint_digest: &[u8; 32],
580 ) -> Result<(), FaultStateFormatError> {
581 if self.entries.len() > state_limit.get() as usize {
582 return Err(FaultStateFormatError::TooManyEntries {
583 max: state_limit.get() as usize,
584 });
585 }
586 if self
587 .entries
588 .iter()
589 .any(|entry| entry.ordinal().get() > retry_limit.get())
590 {
591 return Err(FaultStateFormatError::OrdinalAboveLimit {
592 max: retry_limit.get(),
593 });
594 }
595 if !self.entries.is_empty() && &self.checkpoint_digest != checkpoint_digest {
596 return Err(FaultStateFormatError::CheckpointMismatch);
597 }
598 Ok(())
599 }
600}
601
602impl Default for FaultStateEnvelope {
603 fn default() -> Self {
604 Self::empty()
605 }
606}
607
608#[derive(Clone, Copy, Debug, Eq, PartialEq)]
613#[non_exhaustive]
614pub enum FaultStateFormatError {
615 UnsupportedFormat,
617 UnsupportedSchemaVersion,
619 Malformed,
621 MalformedEntry,
623 UnknownEnumeration,
625 ChecksumMismatch,
627 TooLarge {
629 max_bytes: usize,
631 },
632 TooManyEntries {
634 max: usize,
636 },
637 DuplicateKey,
639 UnsortedEntries,
641 OrdinalAboveLimit {
643 max: u32,
645 },
646 CheckpointMismatch,
648}
649
650impl fmt::Display for FaultStateFormatError {
651 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
652 match self {
653 Self::UnsupportedFormat => formatter.write_str("fault-state format is unsupported"),
654 Self::UnsupportedSchemaVersion => {
655 formatter.write_str("fault-state schema version is unsupported")
656 }
657 Self::Malformed => formatter.write_str("fault state is malformed"),
658 Self::MalformedEntry => formatter.write_str("fault-state entry is malformed"),
659 Self::UnknownEnumeration => {
660 formatter.write_str("fault state contains an unknown enumeration value")
661 }
662 Self::ChecksumMismatch => formatter.write_str("fault-state checksum does not match"),
663 Self::TooLarge { max_bytes } => {
664 write!(formatter, "fault state exceeds {max_bytes} bytes")
665 }
666 Self::TooManyEntries { max } => {
667 write!(formatter, "fault state retains more than {max} keys")
668 }
669 Self::DuplicateKey => formatter.write_str("fault state repeats one retry key"),
670 Self::UnsortedEntries => formatter.write_str("fault state is not digest-sorted"),
671 Self::OrdinalAboveLimit { max } => {
672 write!(formatter, "fault state retains an ordinal above {max}")
673 }
674 Self::CheckpointMismatch => {
675 formatter.write_str("fault state belongs to a superseded checkpoint")
676 }
677 }
678 }
679}
680
681impl Error for FaultStateFormatError {}
682
683fn hex(bytes: &[u8; 32]) -> String {
684 let mut text = String::with_capacity(64);
685 for byte in bytes {
686 text.push(char::from_digit(u32::from(byte >> 4), 16).unwrap_or('0'));
687 text.push(char::from_digit(u32::from(byte & 0x0f), 16).unwrap_or('0'));
688 }
689 text
690}
691
692fn unhex(text: &str) -> Option<[u8; 32]> {
693 if text.len() != 64 {
694 return None;
695 }
696 let mut bytes = [0_u8; 32];
697 let raw = text.as_bytes();
698 for (index, slot) in bytes.iter_mut().enumerate() {
699 let high = char::from(raw[index * 2]).to_digit(16)?;
700 let low = char::from(raw[index * 2 + 1]).to_digit(16)?;
701 *slot = u8::try_from(high * 16 + low).ok()?;
702 }
703 Some(bytes)
704}
705
706pub trait FaultStateStore: Send + Sync {
712 fn bind(
718 &self,
719 _context: ChunkTransactionContext,
720 ) -> BoxFuture<'_, Result<(), FaultStateError>> {
721 Box::pin(std::future::ready(Ok(())))
722 }
723
724 fn reserved_ordinal(
726 &self,
727 key: RetryKey,
728 ) -> BoxFuture<'_, Result<Option<RetryOrdinal>, FaultStateError>>;
729
730 fn reserve(&self, reservation: RetryReservation) -> BoxFuture<'_, Result<(), FaultStateError>>;
732
733 fn resolve(&self, key: RetryKey) -> BoxFuture<'_, Result<(), FaultStateError>>;
738
739 fn clear_resolved(&self) -> BoxFuture<'_, Result<(), FaultStateError>>;
741
742 fn unresolved(&self) -> BoxFuture<'_, Result<u32, FaultStateError>>;
744}
745
746#[derive(Clone, Copy, Debug)]
747struct RetryEntry {
748 ordinal: RetryOrdinal,
749 resolved: bool,
750}
751
752#[derive(Debug)]
759pub struct InMemoryFaultState {
760 limit: RetryStateLimit,
761 entries: Mutex<BTreeMap<RetryKey, RetryEntry>>,
762}
763
764impl InMemoryFaultState {
765 #[must_use]
767 pub fn new(limit: RetryStateLimit) -> Self {
768 Self {
769 limit,
770 entries: Mutex::new(BTreeMap::new()),
771 }
772 }
773
774 fn with_entries<T>(&self, body: impl FnOnce(&mut BTreeMap<RetryKey, RetryEntry>) -> T) -> T {
775 let mut entries = self.entries.lock().unwrap_or_else(PoisonError::into_inner);
776 body(&mut entries)
777 }
778}
779
780impl FaultStateStore for InMemoryFaultState {
781 fn reserved_ordinal(
782 &self,
783 key: RetryKey,
784 ) -> BoxFuture<'_, Result<Option<RetryOrdinal>, FaultStateError>> {
785 let result = self.with_entries(|entries| entries.get(&key).map(|entry| entry.ordinal));
786 Box::pin(std::future::ready(Ok(result)))
787 }
788
789 fn reserve(&self, reservation: RetryReservation) -> BoxFuture<'_, Result<(), FaultStateError>> {
790 let limit = self.limit;
791 let result = self.with_entries(|entries| {
792 let expected = entries
793 .get(&reservation.key())
794 .map_or(RetryOrdinal::INITIAL, |entry| entry.ordinal)
795 .checked_next()
796 .map_err(|_| FaultStateError::StaleReservation)?;
797 if reservation.ordinal() != expected {
798 return Err(FaultStateError::StaleReservation);
799 }
800 let unresolved = entries.values().filter(|entry| !entry.resolved).count();
801 let is_new = !entries.contains_key(&reservation.key());
802 if is_new && unresolved >= limit.get() as usize {
803 return Err(FaultStateError::CapacityExhausted { max: limit.get() });
804 }
805 entries.insert(
806 reservation.key(),
807 RetryEntry {
808 ordinal: reservation.ordinal(),
809 resolved: false,
810 },
811 );
812 Ok(())
813 });
814 Box::pin(std::future::ready(result))
815 }
816
817 fn resolve(&self, key: RetryKey) -> BoxFuture<'_, Result<(), FaultStateError>> {
818 self.with_entries(|entries| {
819 if let Some(entry) = entries.get_mut(&key) {
820 entry.resolved = true;
821 }
822 });
823 Box::pin(std::future::ready(Ok(())))
824 }
825
826 fn clear_resolved(&self) -> BoxFuture<'_, Result<(), FaultStateError>> {
827 self.with_entries(|entries| entries.retain(|_, entry| !entry.resolved));
828 Box::pin(std::future::ready(Ok(())))
829 }
830
831 fn unresolved(&self) -> BoxFuture<'_, Result<u32, FaultStateError>> {
832 let count = self.with_entries(|entries| entries.values().filter(|e| !e.resolved).count());
833 let result = u32::try_from(count).map_err(|_| FaultStateError::Unavailable);
834 Box::pin(std::future::ready(result))
835 }
836}
837
838#[derive(Clone)]
895pub struct FaultRuntime {
896 policy: Arc<FaultPolicy>,
897 sleeper: Arc<dyn BackoffSleeper>,
898 state: Arc<dyn FaultStateStore>,
899 delivery_mode: ChunkDeliveryMode,
900}
901
902impl FaultRuntime {
903 pub fn new(
911 policy: FaultPolicy,
912 sleeper: Arc<dyn BackoffSleeper>,
913 state: Arc<dyn FaultStateStore>,
914 delivery_mode: ChunkDeliveryMode,
915 ) -> Result<Self, crate::FaultPolicyError> {
916 policy.validate_capabilities(matches!(
917 delivery_mode,
918 ChunkDeliveryMode::AtomicSameResource
919 ))?;
920 Ok(Self {
921 policy: Arc::new(policy),
922 sleeper,
923 state,
924 delivery_mode,
925 })
926 }
927
928 #[must_use]
930 pub fn policy(&self) -> &FaultPolicy {
931 &self.policy
932 }
933
934 #[must_use]
936 pub fn sleeper(&self) -> &dyn BackoffSleeper {
937 self.sleeper.as_ref()
938 }
939
940 #[must_use]
942 pub fn state(&self) -> &dyn FaultStateStore {
943 self.state.as_ref()
944 }
945
946 #[must_use]
948 pub const fn delivery_mode(&self) -> ChunkDeliveryMode {
949 self.delivery_mode
950 }
951}
952
953impl fmt::Debug for FaultRuntime {
954 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
955 formatter
956 .debug_struct("FaultRuntime")
957 .field("retry_limit", &self.policy.retry_limit())
958 .field("retry_state_limit", &self.policy.retry_state_limit())
959 .field("skip_limit", &self.policy.skip_limit())
960 .field("backoff", &self.policy.backoff().kind())
961 .field("delivery_mode", &self.delivery_mode)
962 .finish_non_exhaustive()
963 }
964}
965
966#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
972pub struct FaultProgress {
973 retries: RetryCounts,
974 skips: SkipCounts,
975 rollbacks: u64,
976 no_rollbacks: u64,
977}
978
979impl FaultProgress {
980 pub const NONE: Self = Self {
982 retries: RetryCounts::ZERO,
983 skips: SkipCounts::ZERO,
984 rollbacks: 0,
985 no_rollbacks: 0,
986 };
987
988 #[must_use]
990 pub const fn new(
991 retries: RetryCounts,
992 skips: SkipCounts,
993 rollbacks: u64,
994 no_rollbacks: u64,
995 ) -> Self {
996 Self {
997 retries,
998 skips,
999 rollbacks,
1000 no_rollbacks,
1001 }
1002 }
1003
1004 #[must_use]
1006 pub const fn retries(self) -> RetryCounts {
1007 self.retries
1008 }
1009
1010 #[must_use]
1012 pub const fn skips(self) -> SkipCounts {
1013 self.skips
1014 }
1015
1016 #[must_use]
1018 pub const fn rollbacks(self) -> u64 {
1019 self.rollbacks
1020 }
1021
1022 #[must_use]
1024 pub const fn no_rollbacks(self) -> u64 {
1025 self.no_rollbacks
1026 }
1027}
1028
1029#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
1033pub struct RetryCounts {
1034 read: u64,
1035 process: u64,
1036 write: u64,
1037}
1038
1039impl RetryCounts {
1040 pub const ZERO: Self = Self {
1042 read: 0,
1043 process: 0,
1044 write: 0,
1045 };
1046
1047 #[must_use]
1049 pub const fn new(read: u64, process: u64, write: u64) -> Self {
1050 Self {
1051 read,
1052 process,
1053 write,
1054 }
1055 }
1056
1057 #[must_use]
1059 pub const fn read(self) -> u64 {
1060 self.read
1061 }
1062
1063 #[must_use]
1065 pub const fn process(self) -> u64 {
1066 self.process
1067 }
1068
1069 #[must_use]
1071 pub const fn write(self) -> u64 {
1072 self.write
1073 }
1074
1075 #[must_use]
1079 pub const fn increment(mut self, phase: FaultPhase) -> Self {
1080 let counter = match phase {
1081 FaultPhase::Read => &mut self.read,
1082 FaultPhase::Process => &mut self.process,
1083 FaultPhase::Write => &mut self.write,
1084 _ => return self,
1085 };
1086 *counter = counter.saturating_add(1);
1087 self
1088 }
1089}