1use std::error::Error;
9use std::fmt;
10use std::time::Duration;
11
12use crate::{ChunkDeliveryMode, ClassifierRevision, FailureCategory, FailureId, FailureSummary};
13
14const MAX_BACKOFF: Duration = Duration::from_hours(24);
16const MAX_RETRY: u32 = 65_535;
18const MIN_RETRY_STATE: u32 = 1;
20const MAX_RETRY_STATE: u32 = 256;
22
23#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
28#[non_exhaustive]
29pub enum FaultPhase {
30 Read,
32 Process,
34 Write,
36 Transaction,
38 Checkpoint,
40 Listener,
42 Backoff,
44}
45
46impl FaultPhase {
47 #[must_use]
51 pub const fn is_policy_eligible(self) -> bool {
52 !matches!(self, Self::Listener)
53 }
54
55 #[must_use]
57 pub const fn is_skippable(self) -> bool {
58 matches!(self, Self::Read | Self::Process | Self::Write)
59 }
60
61 #[must_use]
65 pub const fn allows_commit_safe_skip(self) -> bool {
66 matches!(self, Self::Read | Self::Process)
67 }
68
69 #[must_use]
71 pub const fn as_str(self) -> &'static str {
72 match self {
73 Self::Read => "read",
74 Self::Process => "process",
75 Self::Write => "write",
76 Self::Transaction => "transaction",
77 Self::Checkpoint => "checkpoint",
78 Self::Listener => "listener",
79 Self::Backoff => "backoff",
80 }
81 }
82
83 #[must_use]
87 pub fn from_durable_name(value: &str) -> Option<Self> {
88 Some(match value {
89 "read" => Self::Read,
90 "process" => Self::Process,
91 "write" => Self::Write,
92 "transaction" => Self::Transaction,
93 "checkpoint" => Self::Checkpoint,
94 "listener" => Self::Listener,
95 "backoff" => Self::Backoff,
96 _ => return None,
97 })
98 }
99}
100
101impl fmt::Display for FaultPhase {
102 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
103 formatter.write_str(self.as_str())
104 }
105}
106
107#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
112pub struct RetryOrdinal(u16);
113
114impl RetryOrdinal {
115 pub const INITIAL: Self = Self(0);
117
118 pub fn new(value: u32) -> Result<Self, FaultPolicyError> {
124 u16::try_from(value)
125 .map(Self)
126 .map_err(|_| FaultPolicyError::RetryOrdinalOutOfRange { max: MAX_RETRY })
127 }
128
129 #[must_use]
131 #[allow(
132 clippy::cast_lossless,
133 reason = "`From` is not const; the widening cast is exact"
134 )]
135 pub const fn get(self) -> u32 {
136 self.0 as u32
137 }
138
139 #[must_use]
141 pub const fn is_initial(self) -> bool {
142 self.0 == 0
143 }
144
145 pub fn checked_next(self) -> Result<Self, FaultPolicyError> {
152 self.0
153 .checked_add(1)
154 .map(Self)
155 .ok_or(FaultPolicyError::RetryOrdinalOutOfRange { max: MAX_RETRY })
156 }
157}
158
159#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
161pub struct RetryLimit(u16);
162
163impl RetryLimit {
164 pub const NONE: Self = Self(0);
166
167 pub fn new(value: u32) -> Result<Self, FaultPolicyError> {
173 u16::try_from(value)
174 .map(Self)
175 .map_err(|_| FaultPolicyError::RetryLimitOutOfRange { max: MAX_RETRY })
176 }
177
178 #[must_use]
180 #[allow(
181 clippy::cast_lossless,
182 reason = "`From` is not const; the widening cast is exact"
183 )]
184 pub const fn get(self) -> u32 {
185 self.0 as u32
186 }
187
188 #[must_use]
190 pub const fn is_none(self) -> bool {
191 self.0 == 0
192 }
193
194 #[must_use]
198 pub const fn permits(self, ordinal: RetryOrdinal) -> bool {
199 !ordinal.is_initial() && ordinal.0 <= self.0
200 }
201}
202
203#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
205pub struct RetryStateLimit(u16);
206
207impl RetryStateLimit {
208 pub fn new(value: u32) -> Result<Self, FaultPolicyError> {
215 if !(MIN_RETRY_STATE..=MAX_RETRY_STATE).contains(&value) {
216 return Err(FaultPolicyError::RetryStateLimitOutOfRange {
217 min: MIN_RETRY_STATE,
218 max: MAX_RETRY_STATE,
219 });
220 }
221 u16::try_from(value)
222 .map(Self)
223 .map_err(|_| FaultPolicyError::RetryStateLimitOutOfRange {
224 min: MIN_RETRY_STATE,
225 max: MAX_RETRY_STATE,
226 })
227 }
228
229 #[must_use]
231 #[allow(
232 clippy::cast_lossless,
233 reason = "`From` is not const; the widening cast is exact"
234 )]
235 pub const fn get(self) -> u32 {
236 self.0 as u32
237 }
238}
239
240#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
242pub struct SkipLimit(u64);
243
244impl SkipLimit {
245 pub const NONE: Self = Self(0);
247
248 #[must_use]
250 pub const fn new(value: u64) -> Self {
251 Self(value)
252 }
253
254 #[must_use]
256 pub const fn get(self) -> u64 {
257 self.0
258 }
259}
260
261#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
263pub struct SkipCounts {
264 read: u64,
265 process: u64,
266 write: u64,
267}
268
269impl SkipCounts {
270 pub const ZERO: Self = Self {
272 read: 0,
273 process: 0,
274 write: 0,
275 };
276
277 #[must_use]
279 pub const fn new(read: u64, process: u64, write: u64) -> Self {
280 Self {
281 read,
282 process,
283 write,
284 }
285 }
286
287 #[must_use]
289 pub const fn read(self) -> u64 {
290 self.read
291 }
292
293 #[must_use]
295 pub const fn process(self) -> u64 {
296 self.process
297 }
298
299 #[must_use]
301 pub const fn write(self) -> u64 {
302 self.write
303 }
304
305 pub fn checked_total(self) -> Result<u64, FaultPolicyError> {
311 self.read
312 .checked_add(self.process)
313 .and_then(|partial| partial.checked_add(self.write))
314 .ok_or(FaultPolicyError::SkipCountOverflow)
315 }
316
317 pub fn checked_add(self, other: Self) -> Result<Self, FaultPolicyError> {
323 let next = Self {
324 read: self
325 .read
326 .checked_add(other.read)
327 .ok_or(FaultPolicyError::SkipCountOverflow)?,
328 process: self
329 .process
330 .checked_add(other.process)
331 .ok_or(FaultPolicyError::SkipCountOverflow)?,
332 write: self
333 .write
334 .checked_add(other.write)
335 .ok_or(FaultPolicyError::SkipCountOverflow)?,
336 };
337 next.checked_total()?;
338 Ok(next)
339 }
340
341 pub fn checked_increment(self, phase: FaultPhase) -> Result<Self, FaultPolicyError> {
349 let mut next = self;
350 let counter = match phase {
351 FaultPhase::Read => &mut next.read,
352 FaultPhase::Process => &mut next.process,
353 FaultPhase::Write => &mut next.write,
354 other => return Err(FaultPolicyError::PhaseNotSkippable { phase: other }),
355 };
356 *counter = counter
357 .checked_add(1)
358 .ok_or(FaultPolicyError::SkipCountOverflow)?;
359 next.checked_total()?;
360 Ok(next)
361 }
362}
363
364#[derive(Clone, Copy, Debug, Eq, PartialEq)]
369pub struct FaultDescriptor {
370 phase: FaultPhase,
371 summary: FailureSummary,
372 retry_ordinal: RetryOrdinal,
373 committed_skips: SkipCounts,
374 transaction_open: bool,
375 delivery_mode: ChunkDeliveryMode,
376}
377
378impl FaultDescriptor {
379 #[must_use]
381 pub const fn new(
382 phase: FaultPhase,
383 summary: FailureSummary,
384 retry_ordinal: RetryOrdinal,
385 committed_skips: SkipCounts,
386 transaction_open: bool,
387 delivery_mode: ChunkDeliveryMode,
388 ) -> Self {
389 Self {
390 phase,
391 summary,
392 retry_ordinal,
393 committed_skips,
394 transaction_open,
395 delivery_mode,
396 }
397 }
398
399 #[must_use]
401 pub const fn phase(self) -> FaultPhase {
402 self.phase
403 }
404
405 #[must_use]
407 pub const fn summary(self) -> FailureSummary {
408 self.summary
409 }
410
411 #[must_use]
413 pub const fn category(self) -> FailureCategory {
414 self.summary.category()
415 }
416
417 #[must_use]
419 pub const fn failure_id(self) -> FailureId {
420 self.summary.failure_id()
421 }
422
423 #[must_use]
425 pub const fn retry_ordinal(self) -> RetryOrdinal {
426 self.retry_ordinal
427 }
428
429 #[must_use]
431 pub const fn committed_skips(self) -> SkipCounts {
432 self.committed_skips
433 }
434
435 #[must_use]
437 pub const fn is_transaction_open(self) -> bool {
438 self.transaction_open
439 }
440
441 #[must_use]
443 pub const fn delivery_mode(self) -> ChunkDeliveryMode {
444 self.delivery_mode
445 }
446}
447
448#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
450#[non_exhaustive]
451pub enum BackoffKind {
452 None,
454 Fixed,
456 Exponential,
458}
459
460impl BackoffKind {
461 #[must_use]
463 pub const fn as_str(self) -> &'static str {
464 match self {
465 Self::None => "none",
466 Self::Fixed => "fixed",
467 Self::Exponential => "exponential",
468 }
469 }
470}
471
472impl fmt::Display for BackoffKind {
473 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
474 formatter.write_str(self.as_str())
475 }
476}
477
478#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
483pub struct BackoffPolicy {
484 kind: BackoffKind,
485 initial: Duration,
486 multiplier: u32,
487 maximum: Duration,
488}
489
490impl BackoffPolicy {
491 #[must_use]
493 pub const fn none() -> Self {
494 Self {
495 kind: BackoffKind::None,
496 initial: Duration::ZERO,
497 multiplier: 1,
498 maximum: Duration::ZERO,
499 }
500 }
501
502 pub fn fixed(delay: Duration) -> Result<Self, FaultPolicyError> {
508 check_delay(delay)?;
509 Ok(Self {
510 kind: BackoffKind::Fixed,
511 initial: delay,
512 multiplier: 1,
513 maximum: delay,
514 })
515 }
516
517 pub fn exponential(
524 initial: Duration,
525 multiplier: u32,
526 maximum: Duration,
527 ) -> Result<Self, FaultPolicyError> {
528 check_delay(initial)?;
529 check_delay(maximum)?;
530 if multiplier == 0 {
531 return Err(FaultPolicyError::ZeroBackoffMultiplier);
532 }
533 if maximum < initial {
534 return Err(FaultPolicyError::BackoffMaximumBelowInitial);
535 }
536 Ok(Self {
537 kind: BackoffKind::Exponential,
538 initial,
539 multiplier,
540 maximum,
541 })
542 }
543
544 #[must_use]
546 pub const fn kind(self) -> BackoffKind {
547 self.kind
548 }
549
550 #[must_use]
552 pub const fn initial(self) -> Duration {
553 self.initial
554 }
555
556 #[must_use]
558 pub const fn multiplier(self) -> u32 {
559 self.multiplier
560 }
561
562 #[must_use]
564 pub const fn maximum(self) -> Duration {
565 self.maximum
566 }
567
568 #[must_use]
574 pub fn delay_for(self, ordinal: RetryOrdinal) -> Duration {
575 if ordinal.is_initial() {
576 return Duration::ZERO;
577 }
578 match self.kind {
579 BackoffKind::None => Duration::ZERO,
580 BackoffKind::Fixed => self.initial,
581 BackoffKind::Exponential => self.exponential_delay(ordinal.get()),
582 }
583 }
584
585 fn exponential_delay(self, ordinal: u32) -> Duration {
586 let maximum_nanos = self.maximum.as_nanos();
587 let mut nanos = self.initial.as_nanos();
588 if nanos >= maximum_nanos {
589 return self.maximum;
590 }
591 if self.multiplier > 1 {
592 let factor = u128::from(self.multiplier);
593 for _ in 1..ordinal {
594 nanos = nanos.saturating_mul(factor);
595 if nanos >= maximum_nanos {
596 return self.maximum;
597 }
598 }
599 }
600 u64::try_from(nanos).map_or(self.maximum, Duration::from_nanos)
601 }
602}
603
604fn check_delay(delay: Duration) -> Result<(), FaultPolicyError> {
605 if delay > MAX_BACKOFF {
606 return Err(FaultPolicyError::BackoffDelayTooLong {
607 max_seconds: MAX_BACKOFF.as_secs(),
608 });
609 }
610 Ok(())
611}
612
613#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
615#[non_exhaustive]
616pub enum RollbackDisposition {
617 Rollback,
619 CommitSafeSkip,
624}
625
626impl RollbackDisposition {
627 #[must_use]
629 pub const fn as_str(self) -> &'static str {
630 match self {
631 Self::Rollback => "rollback",
632 Self::CommitSafeSkip => "commit_safe_skip",
633 }
634 }
635}
636
637#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
639pub struct FaultAction {
640 retryable: bool,
641 skip: Option<RollbackDisposition>,
642}
643
644impl FaultAction {
645 #[must_use]
647 pub const fn fail() -> Self {
648 Self {
649 retryable: false,
650 skip: None,
651 }
652 }
653
654 #[must_use]
656 pub const fn retry() -> Self {
657 Self {
658 retryable: true,
659 skip: None,
660 }
661 }
662
663 #[must_use]
665 pub const fn skip(disposition: RollbackDisposition) -> Self {
666 Self {
667 retryable: false,
668 skip: Some(disposition),
669 }
670 }
671
672 #[must_use]
674 pub const fn retry_then_skip(disposition: RollbackDisposition) -> Self {
675 Self {
676 retryable: true,
677 skip: Some(disposition),
678 }
679 }
680
681 #[must_use]
683 pub const fn is_retryable(self) -> bool {
684 self.retryable
685 }
686
687 #[must_use]
689 pub const fn skip_disposition(self) -> Option<RollbackDisposition> {
690 self.skip
691 }
692}
693
694#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
696pub struct FaultRule {
697 phase: FaultPhase,
698 category: FailureCategory,
699 action: FaultAction,
700}
701
702impl FaultRule {
703 pub fn new(
710 phase: FaultPhase,
711 category: FailureCategory,
712 action: FaultAction,
713 ) -> Result<Self, FaultPolicyError> {
714 if !phase.is_policy_eligible() || !category.is_policy_eligible() {
715 return Err(FaultPolicyError::NotPolicyEligible { phase, category });
716 }
717 if action.skip.is_some() && !phase.is_skippable() {
718 return Err(FaultPolicyError::PhaseNotSkippable { phase });
719 }
720 if action.skip == Some(RollbackDisposition::CommitSafeSkip)
721 && !phase.allows_commit_safe_skip()
722 {
723 return Err(FaultPolicyError::CommitSafeSkipPhase { phase });
724 }
725 Ok(Self {
726 phase,
727 category,
728 action,
729 })
730 }
731
732 #[must_use]
734 pub const fn phase(self) -> FaultPhase {
735 self.phase
736 }
737
738 #[must_use]
740 pub const fn category(self) -> FailureCategory {
741 self.category
742 }
743
744 #[must_use]
746 pub const fn action(self) -> FaultAction {
747 self.action
748 }
749}
750
751#[derive(Clone, Debug, Eq, PartialEq)]
757pub struct FaultClassifier {
758 revision: ClassifierRevision,
759 rules: Box<[FaultRule]>,
760}
761
762impl FaultClassifier {
763 pub fn new(
772 revision: ClassifierRevision,
773 rules: impl IntoIterator<Item = FaultRule>,
774 ) -> Result<Self, FaultPolicyError> {
775 let mut accepted: Vec<FaultRule> = Vec::new();
776 for rule in rules {
777 if accepted
778 .iter()
779 .any(|existing| existing.phase == rule.phase && existing.category == rule.category)
780 {
781 return Err(FaultPolicyError::DuplicateRule {
782 phase: rule.phase,
783 category: rule.category,
784 });
785 }
786 accepted.push(rule);
787 }
788 Ok(Self {
789 revision,
790 rules: accepted.into_boxed_slice(),
791 })
792 }
793
794 #[must_use]
796 pub const fn revision(&self) -> &ClassifierRevision {
797 &self.revision
798 }
799
800 #[must_use]
802 pub fn rules(&self) -> &[FaultRule] {
803 &self.rules
804 }
805
806 #[must_use]
810 pub fn action_for(&self, phase: FaultPhase, category: FailureCategory) -> Option<FaultAction> {
811 self.rules
812 .iter()
813 .find(|rule| rule.phase == phase && rule.category == category)
814 .map(|rule| rule.action)
815 }
816}
817
818#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
823pub struct FaultEvidence {
824 located: bool,
825 known_rollback: bool,
826 forward_checkpoint_proof: bool,
827}
828
829impl FaultEvidence {
830 pub const NONE: Self = Self {
832 located: false,
833 known_rollback: false,
834 forward_checkpoint_proof: false,
835 };
836
837 #[must_use]
839 pub const fn new(located: bool, known_rollback: bool, forward_checkpoint_proof: bool) -> Self {
840 Self {
841 located,
842 known_rollback,
843 forward_checkpoint_proof,
844 }
845 }
846
847 #[must_use]
849 pub const fn with_located(mut self, located: bool) -> Self {
850 self.located = located;
851 self
852 }
853
854 #[must_use]
856 pub const fn with_known_rollback(mut self, known_rollback: bool) -> Self {
857 self.known_rollback = known_rollback;
858 self
859 }
860
861 #[must_use]
863 pub const fn with_forward_checkpoint_proof(mut self, proof: bool) -> Self {
864 self.forward_checkpoint_proof = proof;
865 self
866 }
867
868 #[must_use]
870 pub const fn is_located(self) -> bool {
871 self.located
872 }
873
874 #[must_use]
876 pub const fn is_known_rollback(self) -> bool {
877 self.known_rollback
878 }
879
880 #[must_use]
882 pub const fn has_forward_checkpoint_proof(self) -> bool {
883 self.forward_checkpoint_proof
884 }
885}
886
887#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
889#[non_exhaustive]
890pub enum FaultDecision {
891 Retry {
893 ordinal: RetryOrdinal,
895 delay: Duration,
897 },
898 Skip {
900 disposition: RollbackDisposition,
902 },
903 FailAndRollback,
905 Unknown,
907 Stop,
909}
910
911impl FaultDecision {
912 #[must_use]
914 pub const fn is_retry(self) -> bool {
915 matches!(self, Self::Retry { .. })
916 }
917
918 #[must_use]
920 pub const fn skip_disposition(self) -> Option<RollbackDisposition> {
921 match self {
922 Self::Skip { disposition } => Some(disposition),
923 _ => None,
924 }
925 }
926}
927
928#[derive(Clone, Debug, Eq, PartialEq)]
933pub struct FaultPolicy {
934 classifier: FaultClassifier,
935 retry_limit: RetryLimit,
936 retry_state_limit: RetryStateLimit,
937 skip_limit: SkipLimit,
938 backoff: BackoffPolicy,
939}
940
941impl FaultPolicy {
942 pub fn new(
949 classifier: FaultClassifier,
950 retry_limit: RetryLimit,
951 retry_state_limit: RetryStateLimit,
952 skip_limit: SkipLimit,
953 backoff: BackoffPolicy,
954 ) -> Result<Self, FaultPolicyError> {
955 if retry_limit.is_none()
956 && let Some(rule) = classifier
957 .rules()
958 .iter()
959 .find(|rule| rule.action().is_retryable())
960 {
961 return Err(FaultPolicyError::UnreachableRetryRule {
962 phase: rule.phase(),
963 category: rule.category(),
964 });
965 }
966 Ok(Self {
967 classifier,
968 retry_limit,
969 retry_state_limit,
970 skip_limit,
971 backoff,
972 })
973 }
974
975 #[must_use]
977 pub const fn classifier(&self) -> &FaultClassifier {
978 &self.classifier
979 }
980
981 #[must_use]
983 pub const fn retry_limit(&self) -> RetryLimit {
984 self.retry_limit
985 }
986
987 #[must_use]
989 pub const fn retry_state_limit(&self) -> RetryStateLimit {
990 self.retry_state_limit
991 }
992
993 #[must_use]
995 pub const fn skip_limit(&self) -> SkipLimit {
996 self.skip_limit
997 }
998
999 #[must_use]
1001 pub const fn backoff(&self) -> BackoffPolicy {
1002 self.backoff
1003 }
1004
1005 #[must_use]
1007 pub fn requires_commit_safe_skip(&self) -> bool {
1008 self.classifier.rules().iter().any(|rule| {
1009 rule.action().skip_disposition() == Some(RollbackDisposition::CommitSafeSkip)
1010 })
1011 }
1012
1013 pub fn validate_capabilities(
1021 &self,
1022 supports_atomic_skip: bool,
1023 ) -> Result<(), FaultPolicyError> {
1024 if self.requires_commit_safe_skip() && !supports_atomic_skip {
1025 return Err(FaultPolicyError::CommitSafeSkipUnsupported);
1026 }
1027 Ok(())
1028 }
1029
1030 #[must_use]
1036 pub fn decide(&self, fault: &FaultDescriptor, evidence: FaultEvidence) -> FaultDecision {
1037 let category = fault.category();
1038 if category == FailureCategory::UnknownCommit {
1039 return FaultDecision::Unknown;
1040 }
1041 if category == FailureCategory::Cancelled {
1042 return FaultDecision::Stop;
1043 }
1044 let phase = fault.phase();
1045 if !phase.is_policy_eligible() || !category.is_policy_eligible() {
1046 return FaultDecision::FailAndRollback;
1047 }
1048 let Some(action) = self.classifier.action_for(phase, category) else {
1049 return FaultDecision::FailAndRollback;
1050 };
1051 if action.is_retryable()
1052 && let Ok(next) = fault.retry_ordinal().checked_next()
1053 && self.retry_limit.permits(next)
1054 {
1055 return FaultDecision::Retry {
1056 ordinal: next,
1057 delay: self.backoff.delay_for(next),
1058 };
1059 }
1060 match action.skip_disposition() {
1061 Some(disposition) => self.decide_skip(fault, disposition, evidence),
1062 None => FaultDecision::FailAndRollback,
1063 }
1064 }
1065
1066 fn decide_skip(
1067 &self,
1068 fault: &FaultDescriptor,
1069 disposition: RollbackDisposition,
1070 evidence: FaultEvidence,
1071 ) -> FaultDecision {
1072 let phase = fault.phase();
1073 if !evidence.is_located() {
1074 return FaultDecision::FailAndRollback;
1075 }
1076 let phase_evidence = match phase {
1077 FaultPhase::Read => evidence.has_forward_checkpoint_proof(),
1078 FaultPhase::Process => true,
1079 FaultPhase::Write => evidence.is_known_rollback(),
1080 _ => false,
1081 };
1082 if !phase_evidence {
1083 return FaultDecision::FailAndRollback;
1084 }
1085 if disposition == RollbackDisposition::CommitSafeSkip
1086 && !(phase.allows_commit_safe_skip()
1087 && evidence.is_known_rollback()
1088 && evidence.has_forward_checkpoint_proof())
1089 {
1090 return FaultDecision::FailAndRollback;
1091 }
1092 match fault.committed_skips().checked_increment(phase) {
1093 Ok(next) => match next.checked_total() {
1094 Ok(total) if total <= self.skip_limit.get() => FaultDecision::Skip { disposition },
1095 _ => FaultDecision::FailAndRollback,
1096 },
1097 Err(_) => FaultDecision::FailAndRollback,
1098 }
1099 }
1100}
1101
1102#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1104#[non_exhaustive]
1105pub enum FaultPolicyError {
1106 RetryLimitOutOfRange {
1108 max: u32,
1110 },
1111 RetryOrdinalOutOfRange {
1113 max: u32,
1115 },
1116 RetryStateLimitOutOfRange {
1118 min: u32,
1120 max: u32,
1122 },
1123 BackoffDelayTooLong {
1125 max_seconds: u64,
1127 },
1128 ZeroBackoffMultiplier,
1130 BackoffMaximumBelowInitial,
1132 NotPolicyEligible {
1134 phase: FaultPhase,
1136 category: FailureCategory,
1138 },
1139 PhaseNotSkippable {
1141 phase: FaultPhase,
1143 },
1144 CommitSafeSkipPhase {
1146 phase: FaultPhase,
1148 },
1149 CommitSafeSkipUnsupported,
1151 DuplicateRule {
1153 phase: FaultPhase,
1155 category: FailureCategory,
1157 },
1158 UnreachableRetryRule {
1160 phase: FaultPhase,
1162 category: FailureCategory,
1164 },
1165 SkipCountOverflow,
1167}
1168
1169impl fmt::Display for FaultPolicyError {
1170 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1171 match self {
1172 Self::RetryLimitOutOfRange { max } => {
1173 write!(formatter, "retry limit exceeds {max}")
1174 }
1175 Self::RetryOrdinalOutOfRange { max } => {
1176 write!(formatter, "retry ordinal exceeds {max}")
1177 }
1178 Self::RetryStateLimitOutOfRange { min, max } => {
1179 write!(formatter, "retry state limit must be within {min}..={max}")
1180 }
1181 Self::BackoffDelayTooLong { max_seconds } => {
1182 write!(formatter, "backoff delay exceeds {max_seconds} seconds")
1183 }
1184 Self::ZeroBackoffMultiplier => {
1185 formatter.write_str("exponential backoff requires a nonzero multiplier")
1186 }
1187 Self::BackoffMaximumBelowInitial => {
1188 formatter.write_str("exponential backoff maximum is below its initial delay")
1189 }
1190 Self::NotPolicyEligible { phase, category } => write!(
1191 formatter,
1192 "{phase} {category:?} faults are never retried or skipped"
1193 ),
1194 Self::PhaseNotSkippable { phase } => {
1195 write!(formatter, "{phase} faults cannot commit a skip")
1196 }
1197 Self::CommitSafeSkipPhase { phase } => {
1198 write!(formatter, "{phase} faults cannot commit a skip safely")
1199 }
1200 Self::CommitSafeSkipUnsupported => {
1201 formatter.write_str("the selected resource cannot commit a skip atomically")
1202 }
1203 Self::DuplicateRule { phase, category } => write!(
1204 formatter,
1205 "duplicate classifier rule for {phase} {category:?}"
1206 ),
1207 Self::UnreachableRetryRule { phase, category } => write!(
1208 formatter,
1209 "{phase} {category:?} retry rule requires a nonzero retry limit"
1210 ),
1211 Self::SkipCountOverflow => formatter.write_str("skip counters overflowed"),
1212 }
1213 }
1214}
1215
1216impl Error for FaultPolicyError {}