Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#131] backoff strategy implemented #144

Merged
merged 1 commit into from
Feb 10, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,13 @@ We can use this token for an entire hour, after that we will receive something l
[{<<"reason">>,<<"ExpiredProviderToken">>}]}
```

## Reconnection

If network goes down or something unexpected happens the `gun` connection with APNs will go down. In that case `apns4erl` will send a message `{reconnecting, ServerPid}` to the client process, that means `apns4erl` lost the connection and it is trying to reconnect. Once the connection has been recover a `{connection_up, ServerPid}` message will be send.


We implemented an *Exponential Backoff* strategy. We can set the *ceiling* time adding the `backoff_ceiling` variable on the `config` file. By default it is set to 10 (seconds).

## Close connections

Apple recommends us to keep our connections open and avoid opening and closing very often. You can check the [Best Practices for Managing Connections](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html) section.
Expand Down
10 changes: 5 additions & 5 deletions rebar.lock
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
{"1.1.0",
[{<<"base64url">>,{pkg,<<"base64url">>,<<"0.0.1">>},0},
[{<<"aleppo">>,{pkg,<<"inaka_aleppo">>,<<"1.0.0">>},3},
{<<"base64url">>,{pkg,<<"base64url">>,<<"0.0.1">>},0},
{<<"cowlib">>,
{git,"https://github.com/ninenines/cowlib",
{ref,"0e7abe0b24593f131add272c275406d8ed231805"}},
1},
{<<"elvis_core">>,{pkg,<<"elvis_core">>,<<"0.3.2">>},1},
{<<"elvis">>,{pkg,<<"elvis_core">>,<<"0.3.2">>},1},
{<<"goldrush">>,{pkg,<<"goldrush">>,<<"0.1.7">>},3},
{<<"gun">>,
{git,"https://github.com/ninenines/gun.git",
{ref,"bc733a2ca5f7d07f997ad6edf184f775b23434aa"}},
0},
{<<"inaka_aleppo">>,{pkg,<<"inaka_aleppo">>,<<"1.0.0">>},3},
{<<"jsx">>,{pkg,<<"jsx">>,<<"2.8.1">>},0},
{<<"katana">>,{pkg,<<"katana">>,<<"0.3.1">>},0},
{<<"katana_code">>,{pkg,<<"katana_code">>,<<"0.1.0">>},2},
Expand All @@ -22,10 +22,10 @@
{<<"zipper">>,{pkg,<<"zipper">>,<<"1.0.0">>},2}]}.
[
{pkg_hash,[
{<<"aleppo">>, <<"8DB14CF16BB8C263C14FF4C3F69F64D7C849D40888944F4204D2CA74F1114CEB">>},
{<<"base64url">>, <<"36A90125F5948E3AFD7BE97662A1504B934DD5DAC78451CA6E9ABF85A10286BE">>},
{<<"elvis_core">>, <<"5424DBD17BEC4265A6808F49064CC1A8D2D1B593B5C4E94E747EDB157ABDE6DE">>},
{<<"elvis">>, <<"5424DBD17BEC4265A6808F49064CC1A8D2D1B593B5C4E94E747EDB157ABDE6DE">>},
{<<"goldrush">>, <<"349A351D17C71C2FDAA18A6C2697562ABE136FEC945F147B381F0CF313160228">>},
{<<"inaka_aleppo">>, <<"8DB14CF16BB8C263C14FF4C3F69F64D7C849D40888944F4204D2CA74F1114CEB">>},
{<<"jsx">>, <<"1453B4EB3615ACB3E2CD0A105D27E6761E2ED2E501AC0B390F5BBEC497669846">>},
{<<"katana">>, <<"7824B4F848E22088872780DD33C0CE6DADBCF2184200D4FA84D9040C9C55496E">>},
{<<"katana_code">>, <<"C34F3926A258D6BEACD8D21F140F3D47D175501936431C460B144339D5271A0B">>},
Expand Down
90 changes: 61 additions & 29 deletions src/apns_connection.erl
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,11 @@
, type := type()
}.

-type state() :: #{ connection := connection()
, gun_connection := pid()
, gun_monitor := reference()
, client := pid()
-type state() :: #{ connection := connection()
, gun_connection := pid()
, client := pid()
, backoff := non_neg_integer()
, backoff_ceiling := non_neg_integer()
}.

%%%===================================================================
Expand Down Expand Up @@ -146,18 +147,27 @@ push_notification(ConnectionName, Token, DeviceId, Notification, Headers) ->
, Headers
}).

%% @doc Waits until receive the `connection_up` message
-spec wait_apns_connection_up(pid()) -> {ok, pid()} | {error, timeout}.
wait_apns_connection_up(Server) ->
receive
{connection_up, Server} -> {ok, Server};
{timeout, Server} -> {error, timeout}
end.

%%%===================================================================
%%% gen_server callbacks
%%%===================================================================

-spec init({connection(), pid()}) -> {ok, State :: state(), timeout()}.
init({Connection, Client}) ->
{GunMonitor, GunConnectionPid} = open_gun_connection(Connection),
GunConnectionPid = open_gun_connection(Connection),

{ok, #{ connection => Connection
, gun_connection => GunConnectionPid
, gun_monitor => GunMonitor
, client => Client
{ok, #{ connection => Connection
, gun_connection => GunConnectionPid
, client => Client
, backoff => 1
, backoff_ceiling => application:get_env(apns, backoff_ceiling, 10)
}, 0}.

-spec handle_call( Request :: term(), From :: {pid(), term()}, State) ->
Expand Down Expand Up @@ -187,15 +197,37 @@ handle_cast(stop, State) ->
handle_cast(_Request, State) ->
{noreply, State}.

-spec handle_info(Info :: timeout() | term(), State) ->
{noreply, State}.
handle_info( {'DOWN', GunMonitor, process, GunConnPid, _}
, #{ gun_connection := GunConnPid
, gun_monitor := GunMonitor
, connection := Connection
-spec handle_info(Info :: timeout() | term(), State) -> {noreply, State}.
handle_info( {gun_down, GunConn, http2, closed, _, _}
, #{ gun_connection := GunConn
, client := Client
, backoff := Backoff
, backoff_ceiling := Ceiling
} = State) ->
{GunMonitor2, GunConnPid2} = open_gun_connection(Connection),
{noreply, State#{gun_connection => GunConnPid2, gun_monitor => GunMonitor2}};
ok = gun:close(GunConn),
Client ! {reconnecting, self()},
Sleep = backoff(Backoff, Ceiling) * 1000, % seconds to wait before reconnect
{ok, _} = timer:send_after(Sleep, reconnect),
{noreply, State#{backoff => Backoff + 1}};
handle_info(reconnect, State) ->
#{ connection := Connection
, client := Client
, backoff := Backoff
, backoff_ceiling := Ceiling
} = State,
GunConn = open_gun_connection(Connection),
{ok, Timeout} = application:get_env(apns, timeout),
case gun:await_up(GunConn, Timeout) of
{ok, http2} ->
Client ! {connection_up, self()},
{noreply, State#{ gun_connection => GunConn
, backoff => 1}};
{error, timeout} ->
ok = gun:close(GunConn),
Sleep = backoff(Backoff, Ceiling) * 1000, % seconds to wait
{ok, _} = timer:send_after(Sleep, reconnect),
{noreply, State#{backoff => Backoff + 1}}
end;
handle_info(timeout, #{gun_connection := GunConn, client := Client} = State) ->
{ok, Timeout} = application:get_env(apns, timeout),
case gun:await_up(GunConn, Timeout) of
Expand Down Expand Up @@ -254,9 +286,7 @@ type(#{type := Type}) ->
%%% Internal Functions
%%%===================================================================

-spec open_gun_connection(connection()) -> { GunMonitor :: reference()
, GunConnectionPid :: pid()
}.
-spec open_gun_connection(connection()) -> GunConnectionPid :: pid().
open_gun_connection(Connection) ->
Host = host(Connection),
Port = port(Connection),
Expand All @@ -275,8 +305,7 @@ open_gun_connection(Connection) ->
, #{ protocols => [http2]
, transport_opts => TransportOpts
}),
GunMonitor = monitor(process, GunConnectionPid),
{GunMonitor, GunConnectionPid}.
GunConnectionPid.

-spec get_headers(apns:headers()) -> list().
get_headers(Headers) ->
Expand All @@ -299,13 +328,6 @@ get_headers(Headers) ->
get_device_path(DeviceId) ->
<<"/3/device/", DeviceId/binary>>.

-spec wait_apns_connection_up(pid()) -> {ok, pid()} | {error, timeout}.
wait_apns_connection_up(Server) ->
receive
{connection_up, Server} -> {ok, Server};
{timeout, Server} -> {error, timeout}
end.

-spec add_authorization_header(apns:headers(), apnd:token()) -> apns:headers().
add_authorization_header(Headers, Token) ->
Headers#{apns_auth_token => <<"bearer ", Token/binary>>}.
Expand All @@ -326,3 +348,13 @@ push(GunConn, DeviceId, HeadersMap, Notification) ->
{Status, ResponseHeaders, DecodedBody};
{error, timeout} -> timeout
end.

-spec backoff(non_neg_integer(), non_neg_integer()) -> non_neg_integer().
backoff(N, Ceiling) ->
case (math:pow(2, N) - 1) of
R when R > Ceiling ->
Ceiling;
NextN ->
NString = float_to_list(NextN, [{decimals, 0}]),
list_to_integer(NString)
end.
13 changes: 8 additions & 5 deletions test/connection_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,14 @@ gun_connection_crashes(_Config) ->
ok = mock_gun_open(),
ok = mock_gun_await_up({ok, http2}),
ConnectionName = my_connection2,
{ok, _ServerPid} = apns:connect(cert, ConnectionName),
{ok, ServerPid} = apns:connect(cert, ConnectionName),
GunPid = apns_connection:gun_connection(ConnectionName),
true = is_process_alive(GunPid),
GunPid ! crash,
GunPid ! {crash, ServerPid},
ktn_task:wait_for(fun() -> is_process_alive(GunPid) end, false),
ktn_task:wait_for(fun() ->
apns_connection:gun_connection(ConnectionName) == GunPid
end, false),
GunPid2 = apns_connection:gun_connection(ConnectionName),
true = is_process_alive(GunPid2),
true = (GunPid =/= GunPid2),
Expand Down Expand Up @@ -248,9 +251,9 @@ default_headers(_Config) ->
-spec test_function() -> ok.
test_function() ->
receive
normal -> ok;
crash -> exit(crashed);
_ -> test_function()
normal -> ok;
{crash, Pid} -> Pid ! {gun_down, self(), http2, closed, [], []};
_ -> test_function()
end.

-spec mock_gun_open() -> ok.
Expand Down