solti_model/domain/policy/
backoff.rs1use std::borrow::Cow;
8use std::hash::{Hash, Hasher};
9
10use serde::{Deserialize, Serialize};
11
12use crate::error::{ModelError, ModelResult};
13
14#[derive(Clone, Debug, Serialize, Deserialize)]
41#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
42#[cfg_attr(feature = "schema", schemars(!try_from, deny_unknown_fields))]
43#[serde(rename_all = "camelCase")]
44#[serde(try_from = "raw::BackoffPolicyRaw")]
45pub struct BackoffPolicy {
46 pub jitter: super::JitterPolicy,
48 #[cfg_attr(feature = "schema", schemars(range(min = 1)))]
50 pub first_ms: u64,
51 #[cfg_attr(feature = "schema", schemars(range(min = 1)))]
53 pub max_ms: u64,
54 #[cfg_attr(feature = "schema", schemars(range(min = 1.0)))]
56 pub factor: f64,
57}
58
59mod raw {
60 use super::*;
61
62 #[derive(Deserialize)]
63 #[serde(rename_all = "camelCase", deny_unknown_fields)]
64 pub(super) struct BackoffPolicyRaw {
65 pub jitter: super::super::JitterPolicy,
66 pub first_ms: u64,
67 pub max_ms: u64,
68 pub factor: f64,
69 }
70
71 impl TryFrom<BackoffPolicyRaw> for BackoffPolicy {
72 type Error = ModelError;
73
74 fn try_from(r: BackoffPolicyRaw) -> Result<Self, Self::Error> {
75 let p = BackoffPolicy {
76 jitter: r.jitter,
77 first_ms: r.first_ms,
78 max_ms: r.max_ms,
79 factor: r.factor,
80 };
81 p.validate()?;
82 Ok(p)
83 }
84 }
85}
86
87impl BackoffPolicy {
88 pub fn validate(&self) -> ModelResult<()> {
106 if self.first_ms == 0 {
107 return Err(ModelError::Invalid(Cow::Borrowed(
108 "backoff first_ms must be greater than zero",
109 )));
110 }
111 if self.max_ms < self.first_ms {
112 return Err(ModelError::Invalid(Cow::Borrowed(
113 "backoff max_ms must be >= first_ms",
114 )));
115 }
116 if !self.factor.is_finite() || self.factor < 1.0 {
117 return Err(ModelError::Invalid(Cow::Borrowed(
118 "backoff factor must be finite and >= 1.0",
119 )));
120 }
121 Ok(())
122 }
123}
124
125impl PartialEq for BackoffPolicy {
126 fn eq(&self, other: &Self) -> bool {
127 self.jitter == other.jitter
128 && self.factor.to_bits() == other.factor.to_bits()
129 && self.first_ms == other.first_ms
130 && self.max_ms == other.max_ms
131 }
132}
133
134impl Eq for BackoffPolicy {}
135
136impl Hash for BackoffPolicy {
137 fn hash<H: Hasher>(&self, state: &mut H) {
138 self.factor.to_bits().hash(state);
139 self.first_ms.hash(state);
140 self.jitter.hash(state);
141 self.max_ms.hash(state);
142 }
143}
144
145impl Default for BackoffPolicy {
146 fn default() -> Self {
148 Self {
149 jitter: super::JitterPolicy::Full,
150 first_ms: 1_000,
151 max_ms: 30_000,
152 factor: 2.0,
153 }
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn validation_accepts_defaults_and_rejects_invalid_fields() {
163 BackoffPolicy::default().validate().unwrap();
164
165 for invalid in [
166 BackoffPolicy {
167 first_ms: 0,
168 ..BackoffPolicy::default()
169 },
170 BackoffPolicy {
171 first_ms: 500,
172 max_ms: 100,
173 ..BackoffPolicy::default()
174 },
175 BackoffPolicy {
176 factor: 0.5,
177 ..BackoffPolicy::default()
178 },
179 BackoffPolicy {
180 factor: f64::NAN,
181 ..BackoffPolicy::default()
182 },
183 ] {
184 assert!(invalid.validate().is_err());
185 }
186 }
187
188 #[test]
189 fn serde_roundtrip_accepts_valid_policy() {
190 let policy = BackoffPolicy::default();
191 let json = serde_json::to_string(&policy).unwrap();
192 let back: BackoffPolicy = serde_json::from_str(&json).unwrap();
193 assert_eq!(back, policy);
194 }
195
196 #[test]
197 fn serde_rejects_every_invalid_field() {
198 for (json, field) in [
199 (
200 r#"{"jitter":"full","firstMs":0,"maxMs":30000,"factor":2.0}"#,
201 "first_ms",
202 ),
203 (
204 r#"{"jitter":"full","firstMs":1000,"maxMs":500,"factor":2.0}"#,
205 "max_ms",
206 ),
207 (
208 r#"{"jitter":"full","firstMs":1000,"maxMs":30000,"factor":0.5}"#,
209 "factor",
210 ),
211 ] {
212 let error = serde_json::from_str::<BackoffPolicy>(json).unwrap_err();
213 assert!(error.to_string().contains(field), "got: {error}");
214 }
215 }
216}