Skip to content

Commit 9b8c4f6

Browse files
authored
Add ability to limit both total dur and num retries (#24)
1 parent eb35334 commit 9b8c4f6

3 files changed

Lines changed: 102 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.5.0](https://github.com/TrueLayer/retry-policies/compare/v0.4.0...v0.5.0) - 2025-04-25
11+
12+
### Changed
13+
14+
- [Breaking] Renamed `build_with_total_retry_duration_and_max_retries` to `build_with_total_retry_duration_and_limit_retries`.
15+
16+
### Added
17+
18+
- Added `build_with_total_retry_duration_and_max_retries`.
19+
1020
## [0.4.0](https://github.com/TrueLayer/retry-policies/compare/v0.3.0...v0.4.0) - 2024-05-10
1121

1222
### Changed
@@ -20,7 +30,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2030
- Remove `fake` dependency
2131

2232
## [0.3.0] - 2024-03-04
23-
- [Breaking] Implement `RetryPolicy` for `ExponentialBackoffTimed`, which requires a modification to the `should_retry` method of
33+
34+
- [Breaking] Implement `RetryPolicy` for `ExponentialBackoffTimed`, which requires a modification to the `should_retry` method of
2435
`RetryPolicy` in order to pass the time at which the task (original request) was started.
2536

2637
## [0.2.1] - 2023-10-09

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "retry-policies"
3-
version = "0.4.0"
3+
version = "0.5.0"
44
authors = ["Luca Palmieri <lpalmieri@truelayer.com>"]
55
edition = "2018"
66
description = "A collection of plug-and-play retry policies for Rust projects."

src/policies/exponential_backoff.rs

Lines changed: 89 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -244,18 +244,22 @@ impl ExponentialBackoffBuilder {
244244
}
245245
}
246246

247-
/// Builds an [`ExponentialBackoff`] with the given maximum total duration and calculates max
248-
/// retries that should happen applying no jitter.
247+
/// Builds an [`ExponentialBackoffTimed`] with the given maximum total duration and limits the
248+
/// number of retries to a calculated maximum.
249249
///
250-
/// For example if we set total duration 24 hours, with retry bounds [1s, 24h] and 2 as base of the exponential,
251-
/// we would calculate 17 max retries, as 1s * pow(2, 16) = 65536s = ~18 hours and 18th attempt would be way
252-
/// after the 24 hours total duration.
250+
/// This calculated maximum is based on how many attempts would be made without jitter applied.
253251
///
254-
/// If the 17th retry ends up being scheduled after 10 hours due to jitter, [`ExponentialBackoff::should_retry`]
255-
/// will return false anyway: no retry will be allowed after total duration.
252+
/// For example if we set total duration 24 hours, with retry bounds [1s, 24h] and 2 as base of
253+
/// the exponential, we would calculate 17 max retries, as 1s * pow(2, 16) = 65536s = ~18 hours
254+
/// and 18th attempt would be way after the 24 hours total duration.
256255
///
257-
/// If one of the 17 allowed retries for some reason (e.g. previous attempts taking a long time) ends up
258-
/// being scheduled after total duration, [`ExponentialBackoff::should_retry`] will return false.
256+
/// If the 17th retry ends up being scheduled after 10 hours due to jitter,
257+
/// [`ExponentialBackoff::should_retry`] will return false anyway: no retry will be allowed
258+
/// after total duration.
259+
///
260+
/// If one of the 17 allowed retries for some reason (e.g. previous attempts taking a long time)
261+
/// ends up being scheduled after total duration, [`ExponentialBackoff::should_retry`] will
262+
/// return false.
259263
///
260264
/// Basically we will enforce whatever comes first, max retries or total duration.
261265
///
@@ -287,7 +291,7 @@ impl ExponentialBackoffBuilder {
287291
/// assert!(matches!(RetryDecision::DoNotRetry, should_retry));
288292
///
289293
/// ```
290-
pub fn build_with_total_retry_duration_and_max_retries(
294+
pub fn build_with_total_retry_duration_and_limit_retries(
291295
self,
292296
total_duration: Duration,
293297
) -> ExponentialBackoffTimed {
@@ -320,6 +324,24 @@ impl ExponentialBackoffBuilder {
320324
},
321325
}
322326
}
327+
328+
/// Builds an [`ExponentialBackoffTimed`] with the given maximum total duration and maximum retries.
329+
pub fn build_with_total_retry_duration_and_max_retries(
330+
self,
331+
total_duration: Duration,
332+
max_n_retries: u32,
333+
) -> ExponentialBackoffTimed {
334+
ExponentialBackoffTimed {
335+
max_total_retry_duration: total_duration,
336+
backoff: ExponentialBackoff {
337+
min_retry_interval: self.min_retry_interval,
338+
max_retry_interval: self.max_retry_interval,
339+
max_n_retries: Some(max_n_retries),
340+
jitter: self.jitter,
341+
base: self.base,
342+
},
343+
}
344+
}
323345
}
324346
#[cfg(test)]
325347
mod tests {
@@ -448,7 +470,7 @@ mod tests {
448470
let backoff = ExponentialBackoff::builder()
449471
// This configuration should allow 17 max retries inside a 24h total duration
450472
.retry_bounds(Duration::from_secs(1), Duration::from_secs(6 * 60 * 60))
451-
.build_with_total_retry_duration_and_max_retries(Duration::from_secs(24 * 60 * 60));
473+
.build_with_total_retry_duration_and_limit_retries(Duration::from_secs(24 * 60 * 60));
452474

453475
{
454476
let started_at = SystemTime::now()
@@ -494,20 +516,73 @@ mod tests {
494516
let backoff_base_2 = ExponentialBackoff::builder()
495517
.retry_bounds(Duration::from_secs(1), Duration::from_secs(60 * 60))
496518
.base(2)
497-
.build_with_total_retry_duration_and_max_retries(Duration::from_secs(60 * 60));
519+
.build_with_total_retry_duration_and_limit_retries(Duration::from_secs(60 * 60));
498520

499521
let backoff_base_3 = ExponentialBackoff::builder()
500522
.retry_bounds(Duration::from_secs(1), Duration::from_secs(60 * 60))
501523
.base(3)
502-
.build_with_total_retry_duration_and_max_retries(Duration::from_secs(60 * 60));
524+
.build_with_total_retry_duration_and_limit_retries(Duration::from_secs(60 * 60));
503525

504526
let backoff_base_4 = ExponentialBackoff::builder()
505527
.retry_bounds(Duration::from_secs(1), Duration::from_secs(60 * 60))
506528
.base(4)
507-
.build_with_total_retry_duration_and_max_retries(Duration::from_secs(60 * 60));
529+
.build_with_total_retry_duration_and_limit_retries(Duration::from_secs(60 * 60));
508530

509531
assert_eq!(backoff_base_2.max_retries().unwrap(), 11);
510532
assert_eq!(backoff_base_3.max_retries().unwrap(), 8);
511533
assert_eq!(backoff_base_4.max_retries().unwrap(), 6);
512534
}
535+
536+
#[test]
537+
fn total_retry_duration_and_max_retries() {
538+
// Create backoff policy with specific duration and retry limits
539+
let backoff = ExponentialBackoff::builder()
540+
.retry_bounds(Duration::from_secs(1), Duration::from_secs(30))
541+
.build_with_total_retry_duration_and_max_retries(Duration::from_secs(120), 5);
542+
543+
// Verify the max retries was set correctly
544+
assert_eq!(backoff.max_retries(), Some(5));
545+
546+
// Test retry within limits
547+
{
548+
let started_at = SystemTime::now()
549+
.checked_sub(Duration::from_secs(60))
550+
.unwrap();
551+
552+
let decision = backoff.should_retry(started_at, 3);
553+
554+
match decision {
555+
RetryDecision::Retry { .. } => {}
556+
_ => panic!("Should retry when within both retry count and duration limits"),
557+
}
558+
}
559+
560+
// Test retry exceed max retries
561+
{
562+
let started_at = SystemTime::now()
563+
.checked_sub(Duration::from_secs(60))
564+
.unwrap();
565+
566+
let decision = backoff.should_retry(started_at, 5);
567+
568+
match decision {
569+
RetryDecision::DoNotRetry => {}
570+
_ => panic!("Should not retry when max retries exceeded"),
571+
}
572+
}
573+
574+
// Test retry exceed duration
575+
{
576+
let started_at = SystemTime::now()
577+
.checked_sub(Duration::from_secs(150))
578+
.unwrap();
579+
580+
let decision = backoff.should_retry(started_at, 3);
581+
582+
match decision {
583+
RetryDecision::DoNotRetry => {}
584+
_ => panic!("Should not retry when time duration exceeded"),
585+
}
586+
}
587+
}
513588
}

0 commit comments

Comments
 (0)