solti_model/domain/policy/
backoff.rs1use std::borrow::Cow;
6use std::hash::{Hash, Hasher};
7
8use serde::{Deserialize, Serialize};
9
10use crate::error::{ModelError, ModelResult};
11
12#[derive(Clone, Debug, Serialize, Deserialize)]
29#[serde(rename_all = "camelCase")]
30#[serde(try_from = "raw::BackoffPolicyRaw")]
31pub struct BackoffPolicy {
32 pub jitter: super::JitterPolicy,
34 pub first_ms: u64,
36 pub max_ms: u64,
38 pub factor: f64,
40}
41
42mod raw {
43 use super::*;
44
45 #[derive(Deserialize)]
46 #[serde(rename_all = "camelCase")]
47 pub(super) struct BackoffPolicyRaw {
48 pub jitter: super::super::JitterPolicy,
49 pub first_ms: u64,
50 pub max_ms: u64,
51 pub factor: f64,
52 }
53
54 impl TryFrom<BackoffPolicyRaw> for BackoffPolicy {
55 type Error = ModelError;
56
57 fn try_from(r: BackoffPolicyRaw) -> Result<Self, Self::Error> {
58 let p = BackoffPolicy {
59 jitter: r.jitter,
60 first_ms: r.first_ms,
61 max_ms: r.max_ms,
62 factor: r.factor,
63 };
64 p.validate()?;
65 Ok(p)
66 }
67 }
68}
69
70impl BackoffPolicy {
71 pub fn validate(&self) -> ModelResult<()> {
78 if self.first_ms == 0 {
79 return Err(ModelError::Invalid(Cow::Borrowed(
80 "backoff first_ms must be greater than zero",
81 )));
82 }
83 if self.max_ms < self.first_ms {
84 return Err(ModelError::Invalid(Cow::Borrowed(
85 "backoff max_ms must be >= first_ms",
86 )));
87 }
88 if !self.factor.is_finite() || self.factor < 1.0 {
89 return Err(ModelError::Invalid(Cow::Borrowed(
90 "backoff factor must be finite and >= 1.0",
91 )));
92 }
93 Ok(())
94 }
95}
96
97impl PartialEq for BackoffPolicy {
98 fn eq(&self, other: &Self) -> bool {
99 self.jitter == other.jitter
100 && self.factor.to_bits() == other.factor.to_bits()
101 && self.first_ms == other.first_ms
102 && self.max_ms == other.max_ms
103 }
104}
105
106impl Eq for BackoffPolicy {}
107
108impl Hash for BackoffPolicy {
109 fn hash<H: Hasher>(&self, state: &mut H) {
110 self.factor.to_bits().hash(state);
111 self.first_ms.hash(state);
112 self.jitter.hash(state);
113 self.max_ms.hash(state);
114 }
115}
116
117impl Default for BackoffPolicy {
118 fn default() -> Self {
120 Self {
121 jitter: super::JitterPolicy::Full,
122 first_ms: 1_000,
123 max_ms: 30_000,
124 factor: 2.0,
125 }
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn validate_accepts_defaults() {
135 assert!(BackoffPolicy::default().validate().is_ok());
136 }
137
138 #[test]
139 fn validate_rejects_zero_first_ms() {
140 let p = BackoffPolicy {
141 first_ms: 0,
142 ..BackoffPolicy::default()
143 };
144 assert!(p.validate().is_err());
145 }
146
147 #[test]
148 fn validate_rejects_max_smaller_than_first() {
149 let p = BackoffPolicy {
150 first_ms: 500,
151 max_ms: 100,
152 ..BackoffPolicy::default()
153 };
154 assert!(p.validate().is_err());
155 }
156
157 #[test]
158 fn validate_rejects_factor_below_one() {
159 let p = BackoffPolicy {
160 factor: 0.5,
161 ..BackoffPolicy::default()
162 };
163 assert!(p.validate().is_err());
164 }
165
166 #[test]
167 fn validate_rejects_nan_factor() {
168 let p = BackoffPolicy {
169 factor: f64::NAN,
170 ..BackoffPolicy::default()
171 };
172 assert!(p.validate().is_err());
173 }
174
175 #[test]
176 fn serde_roundtrip_accepts_valid() {
177 let p = BackoffPolicy::default();
178 let json = serde_json::to_string(&p).unwrap();
179 let back: BackoffPolicy = serde_json::from_str(&json).unwrap();
180 assert_eq!(back, p);
181 }
182
183 #[test]
184 fn serde_rejects_invalid_first_ms_on_deserialize() {
185 let json = r#"{"jitter":"full","firstMs":0,"maxMs":30000,"factor":2.0}"#;
186 let err = serde_json::from_str::<BackoffPolicy>(json).unwrap_err();
187 assert!(err.to_string().contains("first_ms"), "got: {err}");
188 }
189
190 #[test]
191 fn serde_rejects_inverted_max_on_deserialize() {
192 let json = r#"{"jitter":"full","firstMs":1000,"maxMs":500,"factor":2.0}"#;
193 let err = serde_json::from_str::<BackoffPolicy>(json).unwrap_err();
194 assert!(err.to_string().contains("max_ms"), "got: {err}");
195 }
196
197 #[test]
198 fn serde_rejects_factor_below_one_on_deserialize() {
199 let json = r#"{"jitter":"full","firstMs":1000,"maxMs":30000,"factor":0.5}"#;
200 let err = serde_json::from_str::<BackoffPolicy>(json).unwrap_err();
201 assert!(err.to_string().contains("factor"), "got: {err}");
202 }
203}