Skip to content

Commit

Permalink
add jitter option
Browse files Browse the repository at this point in the history
  • Loading branch information
jeffreymeng committed Nov 14, 2024
1 parent 2abf438 commit 083ca63
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 5 deletions.
88 changes: 86 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ version = "0.2.1"
authors = ["Eric Zhang <[email protected]>"]
license = "MIT"
description = "A simple utility for retrying fallible asynchronous operations."
repository = "https://github.com/modal-labs/blobnet"
repository = "https://github.com/modal-labs/named-retry"
documentation = "https://docs.rs/named-retry"
keywords = ["retry", "async"]
categories = ["development-tools::debugging", "asynchronous", "network-programming"]
Expand All @@ -14,6 +14,7 @@ edition = "2021"
[dependencies]
tokio = { version = "1.24.2", features = ["time"] }
tracing = "0.1.32"
rand = "0.8.5"

[dev-dependencies]
tokio = { version = "1.24.2", features = ["rt", "macros", "test-util"] }
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ use named_retry::Retry;
let retry = Retry::new("test")
.attempts(5)
.base_delay(Duration::from_secs(1))
.delay_factor(2.0);
.delay_factor(2.0)
.jitter(true);

let result = retry.run(|| async { Ok::<_, ()>("done!") }).await;
assert_eq!(result, Ok("done!"));
Expand Down
48 changes: 47 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ pub struct Retry {

/// Exponential factor to increase the delay by on each attempt.
pub delay_factor: f64,

/// If true, the delay will be selected randomly from the range [delay/2, delay).
pub enable_jitter: bool,
}

impl Retry {
Expand All @@ -36,6 +39,7 @@ impl Retry {
attempts: 3,
base_delay: Duration::ZERO,
delay_factor: 1.0,
enable_jitter: false,
}
}

Expand All @@ -57,6 +61,21 @@ impl Retry {
self
}

/// Enable jitter.
pub const fn jitter(mut self, enabled: bool) -> Self {
self.enable_jitter = enabled;
self
}

fn apply_jitter(&self, delay: Duration) -> Duration {
if self.enable_jitter {
// [0.5, 1.0)
delay.mul_f64(0.5 + rand::random::<f64>() / 2.0)
} else {
delay
}
}

/// Run a falliable asynchronous function using this retry configuration.
///
/// Panics if the number of attempts is set to `0`, or the base delay is
Expand All @@ -77,7 +96,7 @@ impl Retry {
Err(err) if i == self.attempts - 1 => return Err(err),
Err(err) => {
warn!(?err, "failed retryable operation {}, retrying", self.name);
time::sleep(delay).await;
time::sleep(self.apply_jitter(delay)).await;
delay = delay.mul_f64(self.delay_factor);
}
}
Expand Down Expand Up @@ -153,4 +172,31 @@ mod tests {
assert_eq!(count, 4);
assert!(result.is_ok());
}

#[tokio::test(start_paused = true)]
async fn delayed_retry_with_jitter() {
let start = Instant::now();

let mut count = 0;
// Earliest possible retry is 0s, 50ms, 525ms, 5.525s
let task = Retry::new("test_jitter")
.attempts(4)
.base_delay(Duration::from_millis(100))
.delay_factor(10.0)
.jitter(true)
.run(|| {
count += 1;
async {
println!("elapsed = {:?}", start.elapsed());
if start.elapsed() < Duration::from_millis(500) {
Err::<(), ()>(())
} else {
Ok(())
}
}
});
let result = task.await;
assert_eq!(count, 3);
assert!(result.is_ok());
}
}

0 comments on commit 083ca63

Please sign in to comment.