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}
53
54impl RetryConfig {
55 pub const DEFAULT_ATTEMPTS: u32 = 10;
57 pub const DEFAULT_DELAY: std::time::Duration = std::time::Duration::from_secs(10);
59 pub const DEFAULT_MAX_DELAY: std::time::Duration = std::time::Duration::from_secs(5 * 60);
61
62 pub fn to_policy(&self) -> RetryPolicy {
70 if self.max_delay.duration() < self.delay.duration() {
71 tracing::warn!(
72 "retry.max_delay ({:?}) is less than retry.delay ({:?}); \
73 backoff will be capped at max_delay",
74 self.max_delay.duration(),
75 self.delay.duration(),
76 );
77 }
78 RetryPolicy {
79 max_attempts: self.attempts.max(1),
80 base_delay: self.delay.duration(),
81 max_delay: self.max_delay.duration(),
82 }
83 }
84}
85
86impl Default for RetryConfig {
87 fn default() -> Self {
88 Self {
89 attempts: Self::DEFAULT_ATTEMPTS,
90 delay: HumanDuration(Self::DEFAULT_DELAY),
91 max_delay: HumanDuration(Self::DEFAULT_MAX_DELAY),
92 }
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn defaults_match_goreleaser() {
102 let c = RetryConfig::default();
103 assert_eq!(c.attempts, 10);
104 assert_eq!(c.delay.duration(), std::time::Duration::from_secs(10));
105 assert_eq!(c.max_delay.duration(), std::time::Duration::from_secs(300));
106 }
107
108 #[test]
109 fn empty_yaml_yields_defaults() {
110 let c: RetryConfig = serde_yaml_ng::from_str("{}").unwrap();
111 assert_eq!(c.attempts, 10);
112 assert_eq!(c.delay.duration(), std::time::Duration::from_secs(10));
113 assert_eq!(c.max_delay.duration(), std::time::Duration::from_secs(300));
114 }
115
116 #[test]
117 fn parses_explicit_yaml() {
118 let yaml = r#"
119attempts: 5
120delay: 1s
121max_delay: 30s
122"#;
123 let c: RetryConfig = serde_yaml_ng::from_str(yaml).unwrap();
124 assert_eq!(c.attempts, 5);
125 assert_eq!(c.delay.duration(), std::time::Duration::from_secs(1));
126 assert_eq!(c.max_delay.duration(), std::time::Duration::from_secs(30));
127 }
128
129 #[test]
130 fn parses_compound_humantime() {
131 let yaml = r#"
132attempts: 3
133delay: 500ms
134max_delay: 1h30m
135"#;
136 let c: RetryConfig = serde_yaml_ng::from_str(yaml).unwrap();
137 assert_eq!(c.delay.duration(), std::time::Duration::from_millis(500));
138 assert_eq!(
139 c.max_delay.duration(),
140 std::time::Duration::from_secs(90 * 60),
141 );
142 }
143
144 #[test]
145 fn rejects_unknown_fields() {
146 let yaml = "bogus: 1";
147 let result: Result<RetryConfig, _> = serde_yaml_ng::from_str(yaml);
148 assert!(result.is_err(), "expected deny_unknown_fields to reject");
149 }
150
151 #[test]
152 fn to_policy_round_trip_defaults() {
153 let policy = RetryConfig::default().to_policy();
154 assert_eq!(policy.max_attempts, 10);
155 assert_eq!(policy.base_delay, std::time::Duration::from_secs(10));
156 assert_eq!(policy.max_delay, std::time::Duration::from_secs(300));
157 }
158
159 #[test]
160 fn to_policy_clamps_zero_attempts_to_one() {
161 let c = RetryConfig {
162 attempts: 0,
163 delay: HumanDuration(std::time::Duration::from_secs(1)),
164 max_delay: HumanDuration(std::time::Duration::from_secs(2)),
165 };
166 assert_eq!(c.to_policy().max_attempts, 1);
167 }
168
169 #[test]
170 fn to_policy_max_delay_below_delay_does_not_panic() {
171 let c = RetryConfig {
175 attempts: 3,
176 delay: HumanDuration(std::time::Duration::from_secs(10)),
177 max_delay: HumanDuration(std::time::Duration::from_secs(1)),
178 };
179 let p = c.to_policy();
180 assert_eq!(p.max_attempts, 3);
181 assert_eq!(p.base_delay, std::time::Duration::from_secs(10));
182 assert_eq!(p.max_delay, std::time::Duration::from_secs(1));
183 }
184
185 #[test]
186 fn to_policy_preserves_custom_values() {
187 let c = RetryConfig {
188 attempts: 4,
189 delay: HumanDuration(std::time::Duration::from_millis(250)),
190 max_delay: HumanDuration(std::time::Duration::from_secs(7)),
191 };
192 let p = c.to_policy();
193 assert_eq!(p.max_attempts, 4);
194 assert_eq!(p.base_delay, std::time::Duration::from_millis(250));
195 assert_eq!(p.max_delay, std::time::Duration::from_secs(7));
196 }
197}