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