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