use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::HumanDuration;
use crate::retry::RetryPolicy;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct RetryConfig {
pub attempts: u32,
pub delay: HumanDuration,
pub max_delay: HumanDuration,
}
impl RetryConfig {
pub const DEFAULT_ATTEMPTS: u32 = 10;
pub const DEFAULT_DELAY: std::time::Duration = std::time::Duration::from_secs(10);
pub const DEFAULT_MAX_DELAY: std::time::Duration = std::time::Duration::from_secs(5 * 60);
pub fn to_policy(&self) -> RetryPolicy {
if self.max_delay.duration() < self.delay.duration() {
tracing::warn!(
"retry.max_delay ({:?}) is less than retry.delay ({:?}); \
backoff will be capped at max_delay",
self.max_delay.duration(),
self.delay.duration(),
);
}
RetryPolicy {
max_attempts: self.attempts.max(1),
base_delay: self.delay.duration(),
max_delay: self.max_delay.duration(),
}
}
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
attempts: Self::DEFAULT_ATTEMPTS,
delay: HumanDuration(Self::DEFAULT_DELAY),
max_delay: HumanDuration(Self::DEFAULT_MAX_DELAY),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_match_goreleaser() {
let c = RetryConfig::default();
assert_eq!(c.attempts, 10);
assert_eq!(c.delay.duration(), std::time::Duration::from_secs(10));
assert_eq!(c.max_delay.duration(), std::time::Duration::from_secs(300));
}
#[test]
fn empty_yaml_yields_defaults() {
let c: RetryConfig = serde_yaml_ng::from_str("{}").unwrap();
assert_eq!(c.attempts, 10);
assert_eq!(c.delay.duration(), std::time::Duration::from_secs(10));
assert_eq!(c.max_delay.duration(), std::time::Duration::from_secs(300));
}
#[test]
fn parses_explicit_yaml() {
let yaml = r#"
attempts: 5
delay: 1s
max_delay: 30s
"#;
let c: RetryConfig = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(c.attempts, 5);
assert_eq!(c.delay.duration(), std::time::Duration::from_secs(1));
assert_eq!(c.max_delay.duration(), std::time::Duration::from_secs(30));
}
#[test]
fn parses_compound_humantime() {
let yaml = r#"
attempts: 3
delay: 500ms
max_delay: 1h30m
"#;
let c: RetryConfig = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!(c.delay.duration(), std::time::Duration::from_millis(500));
assert_eq!(
c.max_delay.duration(),
std::time::Duration::from_secs(90 * 60),
);
}
#[test]
fn rejects_unknown_fields() {
let yaml = "bogus: 1";
let result: Result<RetryConfig, _> = serde_yaml_ng::from_str(yaml);
assert!(result.is_err(), "expected deny_unknown_fields to reject");
}
#[test]
fn to_policy_round_trip_defaults() {
let policy = RetryConfig::default().to_policy();
assert_eq!(policy.max_attempts, 10);
assert_eq!(policy.base_delay, std::time::Duration::from_secs(10));
assert_eq!(policy.max_delay, std::time::Duration::from_secs(300));
}
#[test]
fn to_policy_clamps_zero_attempts_to_one() {
let c = RetryConfig {
attempts: 0,
delay: HumanDuration(std::time::Duration::from_secs(1)),
max_delay: HumanDuration(std::time::Duration::from_secs(2)),
};
assert_eq!(c.to_policy().max_attempts, 1);
}
#[test]
fn to_policy_max_delay_below_delay_does_not_panic() {
let c = RetryConfig {
attempts: 3,
delay: HumanDuration(std::time::Duration::from_secs(10)),
max_delay: HumanDuration(std::time::Duration::from_secs(1)),
};
let p = c.to_policy();
assert_eq!(p.max_attempts, 3);
assert_eq!(p.base_delay, std::time::Duration::from_secs(10));
assert_eq!(p.max_delay, std::time::Duration::from_secs(1));
}
#[test]
fn to_policy_preserves_custom_values() {
let c = RetryConfig {
attempts: 4,
delay: HumanDuration(std::time::Duration::from_millis(250)),
max_delay: HumanDuration(std::time::Duration::from_secs(7)),
};
let p = c.to_policy();
assert_eq!(p.max_attempts, 4);
assert_eq!(p.base_delay, std::time::Duration::from_millis(250));
assert_eq!(p.max_delay, std::time::Duration::from_secs(7));
}
}