@@ -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) ]
325347mod 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