Skip to content

Commit 8989678

Browse files
bbalsersafwank
authored andcommitted
Created Retry.Annotation: can annotate a function with @Retry
1 parent c3803b0 commit 8989678

File tree

2 files changed

+253
-0
lines changed

2 files changed

+253
-0
lines changed

lib/retry/annotation.ex

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
defmodule Retry.Annotation do
2+
defmacro __using__(_opts) do
3+
quote do
4+
import Retry.DelayStreams
5+
import Stream, only: [take: 2]
6+
require Retry
7+
require Logger
8+
9+
Module.register_attribute(__MODULE__, :retry_funs, accumulate: true)
10+
11+
@on_definition {Retry.Annotation, :on_def}
12+
@before_compile {Retry.Annotation, :before_compile}
13+
end
14+
end
15+
16+
def on_def(env, kind, name, args, guards, _body) do
17+
retry_opts = Module.get_attribute(env.module, :retry, :no_retry)
18+
19+
unless retry_opts == :no_retry do
20+
Module.put_attribute(env.module, :retry_funs, %{
21+
kind: kind,
22+
name: name,
23+
args: Enum.map(args, &de_underscore_name/1),
24+
guards: guards,
25+
retry_opts: Keyword.update(retry_opts, :with, [], &Enum.to_list/1)
26+
})
27+
28+
Module.delete_attribute(env.module, :retry)
29+
end
30+
end
31+
32+
defmacro before_compile(env) do
33+
retry_funs = Module.get_attribute(env.module, :retry_funs, [])
34+
Module.delete_attribute(env.module, :retry_funs)
35+
36+
override_list =
37+
Enum.map(retry_funs, &gen_override_list/1)
38+
|> List.flatten()
39+
40+
overrides =
41+
quote location: :keep do
42+
defoverridable unquote(override_list)
43+
end
44+
45+
functions =
46+
Enum.map(retry_funs, fn fun ->
47+
body = gen_body(fun)
48+
gen_function(fun, body)
49+
end)
50+
51+
[overrides | functions]
52+
end
53+
54+
defp gen_override_list(%{name: name, args: args}) do
55+
no_default_args_length =
56+
Enum.reduce(args, 0, fn
57+
{:\\, _, _}, acc -> acc
58+
_, acc -> acc + 1
59+
end)
60+
61+
Enum.map(no_default_args_length..length(args), fn i -> {name, i} end)
62+
end
63+
64+
defp gen_function(%{kind: :def, guards: [], name: name, args: args}, body) do
65+
quote location: :keep do
66+
def unquote(name)(unquote_splicing(args)) do
67+
unquote(body)
68+
end
69+
end
70+
end
71+
72+
defp gen_function(%{kind: :def, guards: guards, name: name, args: args}, body) do
73+
quote location: :keep do
74+
def unquote(name)(unquote_splicing(args)) when unquote_splicing(guards) do
75+
unquote(body)
76+
end
77+
end
78+
end
79+
80+
defp gen_function(%{kind: :defp, guards: [], name: name, args: args}, body) do
81+
quote location: :keep do
82+
defp unquote(name)(unquote_splicing(args)) do
83+
unquote(body)
84+
end
85+
end
86+
end
87+
88+
defp gen_function(%{kind: :defp, guards: guards, name: name, args: args}, body) do
89+
quote location: :keep do
90+
defp unquote(name)(unquote_splicing(args)) when unquote_splicing(guards) do
91+
unquote(body)
92+
end
93+
end
94+
end
95+
96+
defp gen_body(fun) do
97+
args =
98+
Enum.map(fun.args, fn
99+
{:\\, _, [arg_name | _]} -> arg_name
100+
arg -> arg
101+
end)
102+
103+
quote location: :keep do
104+
Retry.retry_while unquote(fun.retry_opts) do
105+
case super(unquote_splicing(args)) do
106+
{:error, reason} = error ->
107+
Logger.info(fn ->
108+
"#{__MODULE__}: Retrying function #{unquote(fun.name)}: #{inspect(reason)}"
109+
end)
110+
111+
{:cont, error}
112+
113+
result ->
114+
{:halt, result}
115+
end
116+
end
117+
end
118+
end
119+
120+
defp de_underscore_name({:\\, context, [{name, name_context, name_args} | t]} = arg) do
121+
case to_string(name) do
122+
"_" <> real_name -> {:\\, context, [{String.to_atom(real_name), name_context, name_args} | t]}
123+
_ -> arg
124+
end
125+
end
126+
127+
defp de_underscore_name({name, context, args} = arg) do
128+
case to_string(name) do
129+
"_" <> real_name -> {String.to_atom(real_name), context, args}
130+
_ -> arg
131+
end
132+
end
133+
end

test/retry/annotation_test.exs

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
defmodule Retry.AnnotationTest do
2+
use ExUnit.Case
3+
4+
defmodule Example do
5+
use Retry.Annotation
6+
7+
@retry with: constant_backoff(100) |> take(10)
8+
def default_params(x, _opts \\ []) do
9+
{:ok, x}
10+
end
11+
12+
@retry with: constant_backoff(100) |> take(10)
13+
def process(pid) do
14+
Agent.get_and_update(:test_store, fn s ->
15+
{s, max(0, s - 1)}
16+
end)
17+
|> case do
18+
0 ->
19+
{:ok, 0}
20+
21+
n ->
22+
send(pid, {:attempt, n})
23+
{:error, "attempts remaining #{n}"}
24+
end
25+
end
26+
27+
@retry with: constant_backoff(100) |> take(10)
28+
def process_with_guard(pid, x) when is_pid(pid) and is_binary(x) do
29+
Agent.get_and_update(:test_store, fn s ->
30+
{s, max(0, s - 1)}
31+
end)
32+
|> case do
33+
0 ->
34+
{:ok, x}
35+
36+
n ->
37+
send(pid, {:attempt, n})
38+
{:error, "attempts remaining #{n}"}
39+
end
40+
end
41+
42+
def no_retry(pid) do
43+
send(pid, :no_retry)
44+
{:error, "no_retry"}
45+
end
46+
47+
def wrapper(pid) do
48+
internal(pid)
49+
end
50+
51+
@retry with: constant_backoff(100) |> take(10)
52+
defp internal(pid) do
53+
Agent.get_and_update(:test_store, fn s ->
54+
{s, max(0, s - 1)}
55+
end)
56+
|> case do
57+
0 ->
58+
{:ok, 0}
59+
60+
n ->
61+
send(pid, {:attempt, n})
62+
{:error, "attempts remaining #{n}"}
63+
end
64+
end
65+
end
66+
67+
setup do
68+
{:ok, pid} = Agent.start_link(fn -> 0 end, name: :test_store)
69+
on_exit(fn -> assert_down(pid) end)
70+
71+
:ok
72+
end
73+
74+
test "will not retry function when ok tuple returned" do
75+
assert {:ok, 0} = Example.process(self())
76+
refute_receive {:attempt, _}
77+
end
78+
79+
test "will retry on error tuple until ok tuple is received" do
80+
Agent.update(:test_store, fn _ -> 6 end)
81+
assert {:ok, 0} = Example.process(self())
82+
83+
Enum.each(1..6, fn i ->
84+
assert_receive {:attempt, ^i}
85+
end)
86+
end
87+
88+
test "should not retry function that are not annotated" do
89+
assert {:error, "no_retry"} = Example.no_retry(self())
90+
assert_receive :no_retry
91+
refute_receive :no_retry
92+
end
93+
94+
test "guard clauses are still enforced on override function" do
95+
Agent.update(:test_store, fn _ -> 4 end)
96+
assert {:ok, "hello"} == Example.process_with_guard(self(), "hello")
97+
98+
Enum.each(1..4, fn i -> assert_receive {:attempt, ^i} end)
99+
100+
assert_raise FunctionClauseError, fn ->
101+
Example.process_with_guard(self(), 1)
102+
end
103+
end
104+
105+
test "retries private functions as well" do
106+
Agent.update(:test_store, fn _ -> 7 end)
107+
assert {:ok, 0} = Example.wrapper(self())
108+
Enum.each(1..7, fn i -> assert_receive {:attempt, ^i} end)
109+
110+
assert_raise UndefinedFunctionError, fn ->
111+
Example.internal(self())
112+
end
113+
end
114+
115+
defp assert_down(pid) do
116+
ref = Process.monitor(pid)
117+
Process.exit(pid, :kill)
118+
assert_receive {:DOWN, ^ref, _, _, _}
119+
end
120+
end

0 commit comments

Comments
 (0)