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