harmos_domain/model/operations/
policy.rs1use std::time::Duration;
18use serde::{Deserialize, Serialize};
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(deny_unknown_fields)]
32pub struct OperationPolicy {
33 pub timeout: Duration,
34 pub attempt_timeout: Duration,
35 pub retries: RetryPolicy,
36}
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(rename_all = "snake_case", deny_unknown_fields)]
42pub enum RetryPolicy {
43 Never,
44 OnTransient { max_retries: usize, backoff: Backoff },
45}
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "snake_case", deny_unknown_fields)]
49pub enum Backoff {
50 Fixed { delay: Duration, jitter: Jitter },
51 Exponential { initial: Duration, maximum: Duration, jitter: Jitter },
52}
53
54#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(rename_all = "snake_case", deny_unknown_fields)]
56pub enum Jitter {
57 None,
58 Deterministic,
59}
60
61#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(rename_all = "snake_case")]
63pub enum FailureKind {
64 Permanent,
65 Transient,
66 Ambiguous,
67 Unauthorized,
68 Invalid,
69 Cancelled,
70}
71
72#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
73#[serde(rename_all = "snake_case", deny_unknown_fields)]
74pub enum ReplaySafety {
75 Unknown,
76 Idempotent,
77 Deduplicated,
78}
79
80#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
81pub enum PolicyError {
82 #[error("operation deadlines must be positive (overall {timeout:?}, attempt {attempt_timeout:?})")]
83 Deadline { timeout: Duration, attempt_timeout: Duration },
84
85 #[error("a retry policy needs a positive retry count; use Never for zero")]
86 EmptyRetries,
87
88 #[error("a restart policy needs positive credit and sustained health; use Never for zero")]
89 EmptyRestarts,
90
91 #[error("retry delays must be positive and ordered (initial {initial:?}, maximum {maximum:?})")]
92 Backoff { initial: Duration, maximum: Duration },
93}
94
95impl OperationPolicy {
100 pub fn validate(self) -> Result<(), PolicyError> {
101 if self.timeout.is_zero() || self.attempt_timeout.is_zero() {
102 return Err(PolicyError::Deadline {
103 timeout: self.timeout,
104 attempt_timeout: self.attempt_timeout,
105 });
106 }
107
108 self.retries.validate()
109 }
110}
111
112impl RetryPolicy {
113 pub fn validate(self) -> Result<(), PolicyError> {
114 match self {
115 Self::Never => Ok(()),
116 Self::OnTransient { max_retries: 0, .. } => Err(PolicyError::EmptyRetries),
117 Self::OnTransient { backoff, .. } => backoff.validate(),
118 }
119 }
120
121 pub fn retry_after(self, failed_attempt: usize, failure: FailureKind, replay: ReplaySafety, entropy: u64) -> Option<Duration> {
122 let Self::OnTransient { max_retries, backoff } = self else {
123 return None;
124 };
125 if self.validate().is_err() || failed_attempt >= max_retries {
126 return None;
127 }
128 if !matches!(failure, FailureKind::Transient) && !(failure == FailureKind::Ambiguous && replay != ReplaySafety::Unknown) {
129 return None;
130 }
131
132 backoff.delay(failed_attempt, entropy).ok()
133 }
134}
135
136impl Backoff {
137 pub fn validate(self) -> Result<(), PolicyError> {
138 let (initial, maximum) = match self {
139 Self::Fixed { delay, .. } => (delay, delay),
140 Self::Exponential { initial, maximum, .. } => (initial, maximum),
141 };
142 if initial.is_zero() || maximum < initial {
143 return Err(PolicyError::Backoff { initial, maximum });
144 }
145
146 Ok(())
147 }
148
149 pub fn delay(self, failed_attempt: usize, entropy: u64) -> Result<Duration, PolicyError> {
150 self.validate()?;
151
152 let (nominal, maximum, jitter) = match self {
153 Self::Fixed { delay, jitter } => (delay, delay.saturating_add(from_nanos(delay.as_nanos() / 4)), jitter),
154 Self::Exponential { initial, maximum, jitter } => {
155 let shift = u32::try_from(failed_attempt).unwrap_or(u32::MAX);
156 let nanos = if shift >= 128 || initial.as_nanos() > (u128::MAX >> shift) {
157 maximum.as_nanos()
158 } else {
159 (initial.as_nanos() << shift).min(maximum.as_nanos())
160 };
161
162 (from_nanos(nanos), maximum, jitter)
163 }
164 };
165 if jitter == Jitter::None {
166 return Ok(nominal.min(maximum));
167 }
168 let mixed = u128::from(entropy)
169 .wrapping_mul(0x9e37_79b9_7f4a_7c15)
170 .wrapping_add((failed_attempt as u128).wrapping_mul(17).wrapping_add(11));
171 let nanos = mixed % (nominal.as_nanos() / 4 + 1);
172
173 Ok(nominal.saturating_add(from_nanos(nanos)).min(maximum))
174 }
175}
176
177fn from_nanos(nanos: u128) -> Duration {
183 Duration::new((nanos / 1_000_000_000) as u64, (nanos % 1_000_000_000) as u32)
184}
185
186