anodizer_core/config/
retry.rs1use schemars::JsonSchema;
30use serde::{Deserialize, Serialize};
31
32use super::HumanDuration;
33use crate::retry::RetryPolicy;
34
35#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
40#[serde(default, deny_unknown_fields)]
41pub struct RetryConfig {
42 pub attempts: u32,
45 pub delay: HumanDuration,
48 pub max_delay: HumanDuration,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub max_elapsed: Option<HumanDuration>,
63}
64
65impl RetryConfig {
66 pub const DEFAULT_ATTEMPTS: u32 = 10;
68 pub const DEFAULT_DELAY: std::time::Duration = std::time::Duration::from_secs(10);
70 pub const DEFAULT_MAX_DELAY: std::time::Duration = std::time::Duration::from_secs(5 * 60);
72
73 pub fn to_policy(&self) -> RetryPolicy {
81 if self.max_delay.duration() < self.delay.duration() {
82 tracing::warn!(
83 "retry.max_delay ({:?}) is less than retry.delay ({:?}); \
84 backoff will be capped at max_delay",
85 self.max_delay.duration(),
86 self.delay.duration(),
87 );
88 }
89 RetryPolicy {
90 max_attempts: self.attempts.max(1),
91 base_delay: self.delay.duration(),
92 max_delay: self.max_delay.duration(),
93 }
94 }
95
96 pub fn max_elapsed_duration(&self) -> Option<std::time::Duration> {
98 self.max_elapsed.map(|h| h.duration())
99 }
100}
101
102impl Default for RetryConfig {
103 fn default() -> Self {
104 Self {
105 attempts: Self::DEFAULT_ATTEMPTS,
106 delay: HumanDuration(Self::DEFAULT_DELAY),
107 max_delay: HumanDuration(Self::DEFAULT_MAX_DELAY),
108 max_elapsed: None,
109 }
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn defaults_match_goreleaser() {
119 let c = RetryConfig::default();
120 assert_eq!(c.attempts, 10);
121 assert_eq!(c.delay.duration(), std::time::Duration::from_secs(10));
122 assert_eq!(c.max_delay.duration(), std::time::Duration::from_secs(300));
123 }
124
125 #[test]
126 fn empty_yaml_yields_defaults() {
127 let c: RetryConfig = serde_yaml_ng::from_str("{}").unwrap();
128 assert_eq!(c.attempts, 10);
129 assert_eq!(c.delay.duration(), std::time::Duration::from_secs(10));
130 assert_eq!(c.max_delay.duration(), std::time::Duration::from_secs(300));
131 }
132
133 #[test]
134 fn parses_explicit_yaml() {
135 let yaml = r#"
136attempts: 5
137delay: 1s
138max_delay: 30s
139"#;
140 let c: RetryConfig = serde_yaml_ng::from_str(yaml).unwrap();
141 assert_eq!(c.attempts, 5);
142 assert_eq!(c.delay.duration(), std::time::Duration::from_secs(1));
143 assert_eq!(c.max_delay.duration(), std::time::Duration::from_secs(30));
144 }
145
146 #[test]
147 fn parses_compound_humantime() {
148 let yaml = r#"
149attempts: 3
150delay: 500ms
151max_delay: 1h30m
152"#;
153 let c: RetryConfig = serde_yaml_ng::from_str(yaml).unwrap();
154 assert_eq!(c.delay.duration(), std::time::Duration::from_millis(500));
155 assert_eq!(
156 c.max_delay.duration(),
157 std::time::Duration::from_secs(90 * 60),
158 );
159 }
160
161 #[test]
162 fn rejects_unknown_fields() {
163 let yaml = "bogus: 1";
164 let result: Result<RetryConfig, _> = serde_yaml_ng::from_str(yaml);
165 assert!(result.is_err(), "expected deny_unknown_fields to reject");
166 }
167
168 #[test]
169 fn to_policy_round_trip_defaults() {
170 let policy = RetryConfig::default().to_policy();
171 assert_eq!(policy.max_attempts, 10);
172 assert_eq!(policy.base_delay, std::time::Duration::from_secs(10));
173 assert_eq!(policy.max_delay, std::time::Duration::from_secs(300));
174 }
175
176 #[test]
177 fn to_policy_clamps_zero_attempts_to_one() {
178 let c = RetryConfig {
179 attempts: 0,
180 delay: HumanDuration(std::time::Duration::from_secs(1)),
181 max_delay: HumanDuration(std::time::Duration::from_secs(2)),
182 max_elapsed: None,
183 };
184 assert_eq!(c.to_policy().max_attempts, 1);
185 }
186
187 #[test]
188 fn to_policy_max_delay_below_delay_does_not_panic() {
189 let c = RetryConfig {
193 attempts: 3,
194 delay: HumanDuration(std::time::Duration::from_secs(10)),
195 max_delay: HumanDuration(std::time::Duration::from_secs(1)),
196 max_elapsed: None,
197 };
198 let p = c.to_policy();
199 assert_eq!(p.max_attempts, 3);
200 assert_eq!(p.base_delay, std::time::Duration::from_secs(10));
201 assert_eq!(p.max_delay, std::time::Duration::from_secs(1));
202 }
203
204 #[test]
205 fn to_policy_preserves_custom_values() {
206 let c = RetryConfig {
207 attempts: 4,
208 delay: HumanDuration(std::time::Duration::from_millis(250)),
209 max_delay: HumanDuration(std::time::Duration::from_secs(7)),
210 max_elapsed: None,
211 };
212 let p = c.to_policy();
213 assert_eq!(p.max_attempts, 4);
214 assert_eq!(p.base_delay, std::time::Duration::from_millis(250));
215 assert_eq!(p.max_delay, std::time::Duration::from_secs(7));
216 }
217
218 #[test]
219 fn max_elapsed_defaults_to_none() {
220 let c = RetryConfig::default();
221 assert_eq!(c.max_elapsed, None);
222 assert_eq!(c.max_elapsed_duration(), None);
223 }
224
225 #[test]
226 fn max_elapsed_parses_and_other_fields_default() {
227 let c: RetryConfig = serde_yaml_ng::from_str("max_elapsed: 15m").unwrap();
228 assert_eq!(
229 c.max_elapsed_duration(),
230 Some(std::time::Duration::from_secs(15 * 60))
231 );
232 assert_eq!(c.attempts, 10);
234 assert_eq!(c.delay.duration(), std::time::Duration::from_secs(10));
235 assert_eq!(c.max_delay.duration(), std::time::Duration::from_secs(300));
236 }
237
238 #[test]
239 fn max_elapsed_unset_leaves_accessor_none() {
240 let c: RetryConfig = serde_yaml_ng::from_str("attempts: 5").unwrap();
241 assert_eq!(c.max_elapsed_duration(), None);
242 }
243}