use std::time::{Duration, SystemTime};
use crate::error::{DbxError, DbxResult};
#[derive(Debug, Clone)]
pub struct RetryPolicy {
pub max_retries: u32,
pub backoff_ms: u64,
pub exponential: bool,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_retries: 3,
backoff_ms: 100,
exponential: true,
}
}
}
impl RetryPolicy {
pub fn new(max_retries: u32, backoff_ms: u64, exponential: bool) -> Self {
Self {
max_retries,
backoff_ms,
exponential,
}
}
pub fn no_retry() -> Self {
Self {
max_retries: 0,
backoff_ms: 0,
exponential: false,
}
}
pub fn execute<F, T>(&self, mut f: F) -> DbxResult<T>
where
F: FnMut() -> DbxResult<T>,
{
let mut last_err = None;
let mut delay = self.backoff_ms;
for attempt in 0..=self.max_retries {
match f() {
Ok(v) => return Ok(v),
Err(e) => {
last_err = Some(e);
if attempt < self.max_retries && delay > 0 {
std::thread::sleep(Duration::from_millis(delay));
if self.exponential {
delay = delay.saturating_mul(2);
}
}
}
}
}
Err(last_err.unwrap_or_else(|| DbxError::InvalidArguments("no error recorded".into())))
}
pub fn max_attempts(&self) -> u32 {
self.max_retries + 1
}
}
#[derive(Debug, Clone)]
pub struct JobHistoryEntry {
pub job_id: String,
pub started_at: SystemTime,
pub completed_at: Option<SystemTime>,
pub success: bool,
pub error: Option<String>,
pub attempts: u32,
}
impl JobHistoryEntry {
pub fn start(job_id: impl Into<String>) -> Self {
Self {
job_id: job_id.into(),
started_at: SystemTime::now(),
completed_at: None,
success: false,
error: None,
attempts: 0,
}
}
pub fn complete_success(&mut self, attempts: u32) {
self.completed_at = Some(SystemTime::now());
self.success = true;
self.attempts = attempts;
}
pub fn complete_failure(&mut self, error: String, attempts: u32) {
self.completed_at = Some(SystemTime::now());
self.success = false;
self.error = Some(error);
self.attempts = attempts;
}
pub fn duration(&self) -> Option<Duration> {
self.completed_at
.and_then(|end| end.duration_since(self.started_at).ok())
}
}
#[derive(Debug, Default)]
pub struct JobHistory {
entries: Vec<JobHistoryEntry>,
}
impl JobHistory {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, entry: JobHistoryEntry) {
self.entries.push(entry);
}
pub fn entries(&self) -> &[JobHistoryEntry] {
&self.entries
}
pub fn entries_for(&self, job_id: &str) -> Vec<&JobHistoryEntry> {
self.entries.iter().filter(|e| e.job_id == job_id).collect()
}
pub fn last_for(&self, job_id: &str) -> Option<&JobHistoryEntry> {
self.entries.iter().rev().find(|e| e.job_id == job_id)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
#[test]
fn test_retry_executes_until_success() {
let policy = RetryPolicy {
max_retries: 3,
backoff_ms: 0, exponential: false,
};
let attempts = Arc::new(AtomicUsize::new(0));
let a = attempts.clone();
let result = policy.execute(move || {
let count = a.fetch_add(1, Ordering::SeqCst) + 1;
if count < 3 {
Err(DbxError::Storage("fail".into()))
} else {
Ok(())
}
});
assert!(result.is_ok(), "3번째 시도에서 성공해야 함");
assert_eq!(attempts.load(Ordering::SeqCst), 3);
}
#[test]
fn test_retry_fails_after_max() {
let policy = RetryPolicy {
max_retries: 2,
backoff_ms: 0,
exponential: false,
};
let result = policy.execute::<_, ()>(|| Err(DbxError::Storage("always fail".into())));
assert!(result.is_err(), "max_retries 초과 시 실패해야 함");
}
#[test]
fn test_retry_no_retry_on_success() {
let policy = RetryPolicy::no_retry();
let attempts = Arc::new(AtomicUsize::new(0));
let a = attempts.clone();
let result = policy.execute(move || {
a.fetch_add(1, Ordering::SeqCst);
Ok::<_, DbxError>(42)
});
assert!(result.is_ok());
assert_eq!(result.unwrap(), 42);
assert_eq!(attempts.load(Ordering::SeqCst), 1, "재시도 없이 1번만 실행");
}
#[test]
fn test_retry_max_attempts() {
let policy = RetryPolicy::new(3, 0, false);
assert_eq!(policy.max_attempts(), 4);
}
#[test]
fn test_job_history_entry() {
let mut entry = JobHistoryEntry::start("backup");
assert_eq!(entry.job_id, "backup");
assert!(!entry.success);
entry.complete_success(1);
assert!(entry.success);
assert!(entry.completed_at.is_some());
assert!(entry.duration().is_some());
}
#[test]
fn test_job_history_store() {
let mut history = JobHistory::new();
let mut e1 = JobHistoryEntry::start("backup");
e1.complete_success(1);
history.push(e1);
let mut e2 = JobHistoryEntry::start("backup");
e2.complete_failure("disk full".into(), 3);
history.push(e2);
let backup_entries = history.entries_for("backup");
assert_eq!(backup_entries.len(), 2);
let last = history.last_for("backup").unwrap();
assert!(!last.success);
assert_eq!(last.attempts, 3);
}
}