@@ -14,10 +14,10 @@ defmodule Tesla.Middleware.Retry do
14
14
or the maximum delay specified. This creates an upper bound on the maximum delay
15
15
we can see.
16
16
17
- In order to find the actual delay value we take a random number between 0 and
18
- the maximum delay based on a uniform distribution. This randomness ensures that
19
- our retried requests don't "harmonize" making it harder for the downstream
20
- service to heal.
17
+ In order to find the actual delay value we apply additive noise which is proportional to the
18
+ current desired delay. This ensures that the actual delay is kept within the expected order
19
+ of magnitude, while still having some randomness, which ensures that our retried requests
20
+ don't "harmonize" making it harder for the downstream service to heal.
21
21
22
22
## Example
23
23
@@ -43,6 +43,7 @@ defmodule Tesla.Middleware.Retry do
43
43
- `:max_retries` - maximum number of retries (non-negative integer, defaults to 5)
44
44
- `:max_delay` - maximum delay in milliseconds (positive integer, defaults to 5000)
45
45
- `:should_retry` - function to determine if request should be retried
46
+ - `:jitter_factor` - additive noise proportionality constant (float between 0 and 1, defaults to 0.2)
46
47
"""
47
48
48
49
# Not necessary in Elixir 1.10+
@@ -53,7 +54,8 @@ defmodule Tesla.Middleware.Retry do
53
54
@ defaults [
54
55
delay: 50 ,
55
56
max_retries: 5 ,
56
- max_delay: 5_000
57
+ max_delay: 5_000 ,
58
+ jitter_factor: 0.2
57
59
]
58
60
59
61
@ impl Tesla.Middleware
@@ -65,7 +67,8 @@ defmodule Tesla.Middleware.Retry do
65
67
delay: integer_opt! ( opts , :delay , 1 ) ,
66
68
max_retries: integer_opt! ( opts , :max_retries , 0 ) ,
67
69
max_delay: integer_opt! ( opts , :max_delay , 1 ) ,
68
- should_retry: Keyword . get ( opts , :should_retry , & match? ( { :error , _ } , & 1 ) )
70
+ should_retry: Keyword . get ( opts , :should_retry , & match? ( { :error , _ } , & 1 ) ) ,
71
+ jitter_factor: float_opt! ( opts , :jitter_factor , 0 , 1 )
69
72
}
70
73
71
74
retry ( env , next , context )
@@ -84,7 +87,7 @@ defmodule Tesla.Middleware.Retry do
84
87
res = Tesla . run ( env , next )
85
88
86
89
if context . should_retry . ( res ) do
87
- backoff ( context . max_delay , context . delay , context . retries )
90
+ backoff ( context . max_delay , context . delay , context . retries , context . jitter_factor )
88
91
context = update_in ( context , [ :retries ] , & ( & 1 + 1 ) )
89
92
retry ( env , next , context )
90
93
else
@@ -93,10 +96,16 @@ defmodule Tesla.Middleware.Retry do
93
96
end
94
97
95
98
# Exponential backoff with jitter
96
- defp backoff ( cap , base , attempt ) do
99
+ defp backoff ( cap , base , attempt , jitter_factor ) do
97
100
factor = Bitwise . bsl ( 1 , attempt )
98
101
max_sleep = min ( cap , base * factor )
99
- delay = :rand . uniform ( max_sleep )
102
+
103
+ # This ensures that the delay's order of magnitude is kept intact,
104
+ # while still having some jitter. Generates a value x where 1-jitter_factor <= x <= 1 + jitter_factor
105
+ jitter = 1 + 2 * jitter_factor * :rand . uniform ( ) - jitter_factor
106
+
107
+ # The actual delay is in the range max_sleep * (1 - jitter_factor), max_sleep * (1 + jitter_factor)
108
+ delay = trunc ( max_sleep + jitter )
100
109
101
110
:timer . sleep ( delay )
102
111
end
@@ -109,7 +118,22 @@ defmodule Tesla.Middleware.Retry do
109
118
end
110
119
end
111
120
121
+ defp float_opt! ( opts , key , min , max ) do
122
+ case Keyword . fetch ( opts , key ) do
123
+ { :ok , value } when is_float ( value ) and value >= min and value <= max -> value
124
+ { :ok , invalid } -> invalid_float ( key , invalid , min , max )
125
+ :error -> @ defaults [ key ]
126
+ end
127
+ end
128
+
112
129
defp invalid_integer ( key , value , min ) do
113
130
raise ( ArgumentError , "expected :#{ key } to be an integer >= #{ min } , got #{ inspect ( value ) } " )
114
131
end
132
+
133
+ defp invalid_float ( key , value , min , max ) do
134
+ raise (
135
+ ArgumentError ,
136
+ "expected :#{ key } to be a float >= #{ min } and <= #{ max } , got #{ inspect ( value ) } "
137
+ )
138
+ end
115
139
end
0 commit comments