From 50ff97bf82cd146cb289e5b9fbd864bbcc9c30d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Sat, 27 Jul 2019 16:42:00 +0200 Subject: Add the retry_fun option for different backoff strategies --- doc/src/manual/gun.asciidoc | 26 ++++++++++++++++++++++++++ src/gun.erl | 20 +++++++++++++++++--- test/gun_SUITE.erl | 21 ++++++++++++++++++++- 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/doc/src/manual/gun.asciidoc b/doc/src/manual/gun.asciidoc index cd34537..f8d5406 100644 --- a/doc/src/manual/gun.asciidoc +++ b/doc/src/manual/gun.asciidoc @@ -208,6 +208,7 @@ opts() :: #{ http2_opts => http2_opts(), protocols => [http | http2], retry => non_neg_integer(), + retry_fun => fun(), retry_timeout => pos_integer(), supervise => boolean(), tcp_opts => [gen_tcp:connect_option()], @@ -253,6 +254,29 @@ retry (5):: Number of times Gun will try to reconnect on failure before giving up. +retry_fun - see below:: + +A fun that will be called before every reconnect attempt. It receives +the current number of retries left and the Gun options. It returns the +next number of retries left and the timeout to apply before reconnecting. + +The default fun will remove one to the number of retries and set the +timeout to the `retry_timeout` value. + +The fun must be defined as follow: + +[source,erlang] +---- +fun ((non_neg_integer(), opts()) -> #{ + retries => non_neg_integer(), + timeout => pos_integer() +}) +---- + +The fun will never be called when the `retry` option is set to 0. When +this function returns 0 in the `retries` value, Gun will do one last +reconnect attempt before giving up. + retry_timeout (5000):: Time between retries in milliseconds. @@ -350,6 +374,8 @@ undocumented and must be set to `gun_ws_h`. * *2.0*: The `connect_timeout` option has been split into three options: `domain_lookup_timeout`, `connect_timeout` and when applicable `tls_handshake_timeout`. +* *2.0*: The option `retry_fun` was added. It can be used to + implement different reconnect strategies. * *2.0*: The `transport_opts` option has been split into two options: `tcp_opts` and `tls_opts`. * *2.0*: Introduce the type `req_headers()` and extend the diff --git a/src/gun.erl b/src/gun.erl index bb275f5..74866ec 100644 --- a/src/gun.erl +++ b/src/gun.erl @@ -116,6 +116,8 @@ http2_opts => http2_opts(), protocols => [http | http2], retry => non_neg_integer(), + retry_fun => fun((non_neg_integer(), opts()) + -> #{retries => non_neg_integer(), timeout => pos_integer()}), retry_timeout => pos_integer(), supervise => boolean(), tcp_opts => [gen_tcp:connect_option()], @@ -279,6 +281,8 @@ check_options([Opt = {protocols, L}|Opts]) when is_list(L) -> end; check_options([{retry, R}|Opts]) when is_integer(R), R >= 0 -> check_options(Opts); +check_options([{retry_fun, F}|Opts]) when is_function(F, 2) -> + check_options(Opts); check_options([{retry_timeout, T}|Opts]) when is_integer(T), T >= 0 -> check_options(Opts); check_options([{supervise, B}|Opts]) when B =:= true; B =:= false -> @@ -770,15 +774,25 @@ default_transport(_) -> tcp. %% @todo This is where we would implement the backoff mechanism presumably. not_connected(_, {retries, 0, Reason}, State) -> {stop, {shutdown, Reason}, State}; -not_connected(_, {retries, Retries, _}, State=#state{opts=Opts}) -> - Timeout = maps:get(retry_timeout, Opts, 5000), +not_connected(_, {retries, Retries0, _}, State=#state{opts=Opts}) -> + Fun = maps:get(retry_fun, Opts, fun default_retry_fun/2), + #{ + timeout := Timeout, + retries := Retries + } = Fun(Retries0, Opts), {next_state, domain_lookup, State, - {state_timeout, Timeout, {retries, Retries - 1, not_connected}}}; + {state_timeout, Timeout, {retries, Retries, not_connected}}}; not_connected({call, From}, {stream_info, _}, _) -> {keep_state_and_data, {reply, From, {error, not_connected}}}; not_connected(Type, Event, State) -> handle_common(Type, Event, ?FUNCTION_NAME, State). +default_retry_fun(Retries, Opts) -> + #{ + retries => Retries - 1, + timeout => maps:get(retry_timeout, Opts, 5000) + }. + domain_lookup(_, {retries, Retries, _}, State=#state{host=Host, port=Port, opts=Opts, event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> TransOpts = maps:get(tcp_opts, Opts, []), diff --git a/test/gun_SUITE.erl b/test/gun_SUITE.erl index 4f99594..4526a15 100644 --- a/test/gun_SUITE.erl +++ b/test/gun_SUITE.erl @@ -289,7 +289,7 @@ postpone_request_while_not_connected(_) -> end, timer:sleep(Timeout), %% Start the server so that next retry will result in the client connecting successfully. - {ok, ListenSocket} = gen_tcp:listen(23456, [binary, {active, false}]), + {ok, ListenSocket} = gen_tcp:listen(23456, [binary, {active, false}, {reuseaddr, true}]), {ok, ClientSocket} = gen_tcp:accept(ListenSocket, 5000), %% The client should now be up. {ok, http} = gun:await_up(Pid), @@ -395,6 +395,25 @@ retry_1(_) -> error(timeout) end. +retry_fun(_) -> + doc("Ensure the retry_fun is used when provided."), + {ok, Pid} = gun:open("localhost", 12345, #{ + retry => 5, + retry_fun => fun(_, _) -> #{retries => 0, timeout => 500} end, + retry_timeout => 5000 + }), + Ref = monitor(process, Pid), + After = case os:type() of + {win32, _} -> 2800; + _ -> 800 + end, + receive + {'DOWN', Ref, process, Pid, {shutdown, _}} -> + ok + after After -> + error(shutdown_too_late) + end. + retry_immediately(_) -> doc("Ensure Gun retries immediately."), %% We have to make a first successful connection in order to test this. -- cgit v1.2.3