Skip to content

Commit 9c6d321

Browse files
authored
feat: add support for should_retry with arity of 2 to Retry middleware (#608)
* Add support for should_retry with arity of 2 to Retry middleware * Format Middleware * Change should_retry option to be of arity 1 or 3 instead
1 parent cfee71b commit 9c6d321

File tree

3 files changed

+101
-14
lines changed

3 files changed

+101
-14
lines changed

lib/tesla/middleware.ex

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ defmodule Tesla.Middleware do
1414
or inside tuple in case of dynamic middleware (`Tesla.client/1`):
1515
1616
Tesla.client([{Tesla.Middleware.BaseUrl, "https://example.com"}])
17-
17+
1818
## Ordering
19-
19+
2020
The order in which middleware is defined matters. Note that the order when _sending_ the request
2121
matches the order the middleware was defined in, but the order when _receiving_ the response
2222
is reversed.
23-
23+
2424
For example, `Tesla.Middleware.DecompressResponse` must come _after_ `Tesla.Middleware.JSON`,
2525
otherwise the response isn't decompressed before it reaches the JSON parser.
2626

lib/tesla/middleware/retry.ex

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ defmodule Tesla.Middleware.Retry do
3434
{:ok, _} -> false
3535
{:error, _} -> true
3636
end
37+
# or
38+
plug Tesla.Middleware.Retry, should_retry: fn
39+
{:ok, %{status: status}}, _env, _context when status in [400, 500] -> true
40+
{:ok, _reason}, _env, _context -> false
41+
{:error, _reason}, %Tesla.Env{method: :post}, _context -> false
42+
{:error, _reason}, %Tesla.Env{method: :put}, %{retries: 2} -> false
43+
{:error, _reason}, _env, _context -> true
44+
end
3745
end
3846
```
3947
@@ -42,7 +50,9 @@ defmodule Tesla.Middleware.Retry do
4250
- `:delay` - The base delay in milliseconds (positive integer, defaults to 50)
4351
- `:max_retries` - maximum number of retries (non-negative integer, defaults to 5)
4452
- `:max_delay` - maximum delay in milliseconds (positive integer, defaults to 5000)
45-
- `:should_retry` - function to determine if request should be retried
53+
- `:should_retry` - function with an arity of 1 or 3 used to determine if the request should
54+
be retried the first argument is the result, the second is the env and the third is
55+
the context: options + `:retries` (defaults to a match on `{:error, _reason}`)
4656
- `:jitter_factor` - additive noise proportionality constant
4757
(float between 0 and 1, defaults to 0.2)
4858
"""
@@ -65,7 +75,7 @@ defmodule Tesla.Middleware.Retry do
6575
delay: integer_opt!(opts, :delay, 1),
6676
max_retries: integer_opt!(opts, :max_retries, 0),
6777
max_delay: integer_opt!(opts, :max_delay, 1),
68-
should_retry: Keyword.get(opts, :should_retry, &match?({:error, _}, &1)),
78+
should_retry: should_retry_opt!(opts),
6979
jitter_factor: float_opt!(opts, :jitter_factor, 0, 1)
7080
}
7181

@@ -84,15 +94,26 @@ defmodule Tesla.Middleware.Retry do
8494
defp retry(env, next, context) do
8595
res = Tesla.run(env, next)
8696

87-
if context.should_retry.(res) do
88-
backoff(context.max_delay, context.delay, context.retries, context.jitter_factor)
89-
context = update_in(context, [:retries], &(&1 + 1))
90-
retry(env, next, context)
91-
else
92-
res
97+
{:arity, should_retry_arity} = :erlang.fun_info(context.should_retry, :arity)
98+
99+
cond do
100+
should_retry_arity == 1 and context.should_retry.(res) ->
101+
do_retry(env, next, context)
102+
103+
should_retry_arity == 3 and context.should_retry.(res, env, context) ->
104+
do_retry(env, next, context)
105+
106+
true ->
107+
res
93108
end
94109
end
95110

111+
defp do_retry(env, next, context) do
112+
backoff(context.max_delay, context.delay, context.retries, context.jitter_factor)
113+
context = update_in(context, [:retries], &(&1 + 1))
114+
retry(env, next, context)
115+
end
116+
96117
# Exponential backoff with jitter
97118
defp backoff(cap, base, attempt, jitter_factor) do
98119
factor = Bitwise.bsl(1, attempt)
@@ -124,6 +145,19 @@ defmodule Tesla.Middleware.Retry do
124145
end
125146
end
126147

148+
defp should_retry_opt!(opts) do
149+
case Keyword.get(opts, :should_retry, &match?({:error, _}, &1)) do
150+
should_retry_fun when is_function(should_retry_fun, 1) ->
151+
should_retry_fun
152+
153+
should_retry_fun when is_function(should_retry_fun, 3) ->
154+
should_retry_fun
155+
156+
value ->
157+
invalid_should_retry_fun(value)
158+
end
159+
end
160+
127161
defp invalid_integer(key, value, min) do
128162
raise(ArgumentError, "expected :#{key} to be an integer >= #{min}, got #{inspect(value)}")
129163
end
@@ -134,4 +168,11 @@ defmodule Tesla.Middleware.Retry do
134168
"expected :#{key} to be a float >= #{min} and <= #{max}, got #{inspect(value)}"
135169
)
136170
end
171+
172+
defp invalid_should_retry_fun(value) do
173+
raise(
174+
ArgumentError,
175+
"expected :should_retry to be a function with arity of 1 or 3, got #{inspect(value)}"
176+
)
177+
end
137178
end

test/tesla/middleware/retry_test.exs

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ defmodule Tesla.Middleware.RetryTest do
99
response =
1010
case env.url do
1111
"/ok" -> {:ok, env}
12+
"/maybe" when retries == 2 -> {:error, :nxdomain}
1213
"/maybe" when retries < 5 -> {:error, :econnrefused}
1314
"/maybe" -> {:ok, env}
1415
"/nope" -> {:error, :econnrefused}
@@ -39,9 +40,20 @@ defmodule Tesla.Middleware.RetryTest do
3940
delay: 10,
4041
max_retries: 10,
4142
should_retry: fn
42-
{:ok, %{status: status}} when status in [400, 500] -> true
43-
{:ok, _} -> false
44-
{:error, _} -> true
43+
{:ok, %{status: status}}, _env, _context when status in [400, 500] ->
44+
true
45+
46+
{:ok, _reason}, _env, _context ->
47+
false
48+
49+
{:error, _reason}, %Tesla.Env{method: :post}, _context ->
50+
false
51+
52+
{:error, _reason}, %Tesla.Env{method: :put}, %{retries: 2} ->
53+
false
54+
55+
{:error, _reason}, _env, _context ->
56+
true
4557
end
4658

4759
adapter LaggyAdapter
@@ -74,6 +86,14 @@ defmodule Tesla.Middleware.RetryTest do
7486
ClientWithShouldRetryFunction.get("/retry_status")
7587
end
7688

89+
test "use custom retry determination function matching on env" do
90+
assert {:error, :econnrefused} = ClientWithShouldRetryFunction.post("/maybe", "payload")
91+
end
92+
93+
test "use custom retry determination function matching on context" do
94+
assert {:error, :nxdomain} = ClientWithShouldRetryFunction.put("/maybe", "payload")
95+
end
96+
7797
defmodule DefunctClient do
7898
use Tesla
7999

@@ -171,4 +191,30 @@ defmodule Tesla.Middleware.RetryTest do
171191
ClientWithJitterFactorGt1.get("/ok")
172192
end
173193
end
194+
195+
test "ensures should_retry option is a function with arity of 1 or 3" do
196+
defmodule ClientWithShouldRetryArity0 do
197+
use Tesla
198+
plug Tesla.Middleware.Retry, should_retry: fn -> true end
199+
adapter LaggyAdapter
200+
end
201+
202+
defmodule ClientWithShouldRetryArity2 do
203+
use Tesla
204+
plug Tesla.Middleware.Retry, should_retry: fn _res, _env -> true end
205+
adapter LaggyAdapter
206+
end
207+
208+
assert_raise ArgumentError,
209+
~r/expected :should_retry to be a function with arity of 1 or 3, got #Function<\d.\d+\/0/,
210+
fn ->
211+
ClientWithShouldRetryArity0.get("/ok")
212+
end
213+
214+
assert_raise ArgumentError,
215+
~r/expected :should_retry to be a function with arity of 1 or 3, got #Function<\d.\d+\/2/,
216+
fn ->
217+
ClientWithShouldRetryArity2.get("/ok")
218+
end
219+
end
174220
end

0 commit comments

Comments
 (0)