Skip to content

Commit

Permalink
Merge pull request #185 from inaka/ferigis.183.connections_without_name
Browse files Browse the repository at this point in the history
[#183] create connections without name
  • Loading branch information
Brujo Benavides authored Jun 19, 2017
2 parents f7a04c5 + 9fc2e21 commit 41134c3
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 46 deletions.
30 changes: 24 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,20 +112,34 @@ Example:
```erlang
1> apns:connect(cert, my_first_connection).
{ok,<0.87.0>}
2> apns:connect(#{name => another_cert, apple_host => "api.push.apple.com", apple_host => 443,
certtile => "priv/cert.pem", keyfile => "priv/key.pem", type => cert}).
2> apns:connect(#{name => another_cert, apple_host => "api.push.apple.com", apple_port => 443,
certfile => "priv/cert.pem", keyfile => "priv/key.pem", type => cert}).
3> apns:connect(token, my_second_connection).
{ok,<0.95.0>}
```
Note `cert` and `token` define the type we want.

Although `Apns4erl` is supervising the connections `apns:connect/2` returns the connection `pid` just in case you want to monitor it.
`apns:connect/2` returns the connection `pid`.

## Create Connections without name

In some scenarios we don't want to assign names to the connections instead we want working just with the `pid` (working with a pool of connections for example). If that is the case we use the same `apns:connect/1` and `apns:connect/2` functions but instead of a connection name we put `undefined`:

```erlang
1> apns:connect(cert, undefined).
{ok,<0.127.0>}
2> apns:connect(#{name => undefined, apple_host => "api.push.apple.com", apple_port => 443,
certfile => "priv/cert2.pem", keyfile => "priv/key2-noenc.pem", type => cert}).
{ok,<0.130.0>}
3> apns:connect(token, my_second_connection).
{ok,<0.132.0>}
```

## Push Notifications over `Provider Certificate` connections

In order to send Notifications over `Provider Certificate` connection we will use `apns:push_notification/3,4`.

We will need the connection, a notification, the device ID and some http2 headers. The connection is the `atom` we used when we executed `apns:connect/2` for setting a name, the device ID is provided by Apple, the notification is a `map` with the data we want to send, that map will be encoded to json later and the http2 headers can be explicitly sent as a parameter using `apns:push_notification/4` or can be defined at the `config` file, in that case we would use `apns:push_notification/3`.
We will need the connection, a notification, the device ID and some http2 headers. The connection is the `atom` we used when we executed `apns:connect/2` for setting a name or its `pid`, the device ID is provided by Apple, the notification is a `map` with the data we want to send, that map will be encoded to json later and the http2 headers can be explicitly sent as a parameter using `apns:push_notification/4` or can be defined at the `config` file, in that case we would use `apns:push_notification/3`.

This is the `headers` format:

Expand All @@ -144,7 +158,7 @@ All of them are defined by Apple [here](https://developer.apple.com/library/con
Lets send a Notification.

```erlang
1> apns:connect(cert, my_first_connection).
1> {ok, Pid} = apns:connect(cert, my_first_connection).
{ok,<0.85.0>}
2> DeviceId = <<"a0dc63fb059cb9c13b03e5c974af3dd33d67fed4147da8c5ada0626439e18935">>.
<<"a0dc63fb059cb9c13b03e5c974af3dd33d67fed4147da8c5ada0626439e18935">>
Expand All @@ -154,6 +168,10 @@ Lets send a Notification.
{200,
[{<<"apns-id">>,<<"EFDE0D9D-F60C-30F4-3FF1-86F3B90BE434">>}],
no_body}
5> apns:push_notification(Pid, DeviceId, Notification).
{200,
[{<<"apns-id">>,<<"EFDE0D9D-F60C-30F4-3FF1-86F3B90BE654">>}],
no_body}
```

The result is the response itself, its format is:
Expand Down Expand Up @@ -239,7 +257,7 @@ You should check your client inbox after a timeout but it is not guaranteed your

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.

But when closing a connection makes sense `apns4erl` gives us the function `apns:close_connection/1` where the parameter is the connection's name. After using it the name will be available for new connections again.
But when closing a connection makes sense `apns4erl` gives us the function `apns:close_connection/1` where the parameter is the connection's name or the connection's `pid`. After using it the name will be available for new connections again (if it was different than `undefined`).

## Feedback

Expand Down
30 changes: 15 additions & 15 deletions src/apns.erl
Original file line number Diff line number Diff line change
Expand Up @@ -89,55 +89,55 @@ connect(Connection) ->
apns_sup:create_connection(Connection).

%% @doc Closes the connection with APNs service.
-spec close_connection(apns_connection:name()) -> ok.
close_connection(ConnectionName) ->
apns_connection:close_connection(ConnectionName).
-spec close_connection(apns_connection:name() | pid()) -> ok.
close_connection(ConnectionId) ->
apns_connection:close_connection(ConnectionId).

%% @doc Push notification to APNs. It will use the headers provided on the
%% environment variables.
-spec push_notification( apns_connection:name()
-spec push_notification( apns_connection:name() | pid()
, device_id()
, json()
) -> response().
push_notification(ConnectionName, DeviceId, JSONMap) ->
push_notification(ConnectionId, DeviceId, JSONMap) ->
Headers = default_headers(),
push_notification(ConnectionName, DeviceId, JSONMap, Headers).
push_notification(ConnectionId, DeviceId, JSONMap, Headers).

%% @doc Push notification to certificate APNs Connection.
-spec push_notification( apns_connection:name()
-spec push_notification( apns_connection:name() | pid()
, device_id()
, json()
, headers()
) -> response().
push_notification(ConnectionName, DeviceId, JSONMap, Headers) ->
push_notification(ConnectionId, DeviceId, JSONMap, Headers) ->
Notification = jsx:encode(JSONMap),
apns_connection:push_notification( ConnectionName
apns_connection:push_notification( ConnectionId
, DeviceId
, Notification
, Headers
).

%% @doc Push notification to APNs with authentication token. It will use the
%% headers provided on the environment variables.
-spec push_notification_token( apns_connection:name()
-spec push_notification_token( apns_connection:name() | pid()
, token()
, device_id()
, json()
) -> response().
push_notification_token(ConnectionName, Token, DeviceId, JSONMap) ->
push_notification_token(ConnectionId, Token, DeviceId, JSONMap) ->
Headers = default_headers(),
push_notification_token(ConnectionName, Token, DeviceId, JSONMap, Headers).
push_notification_token(ConnectionId, Token, DeviceId, JSONMap, Headers).

%% @doc Push notification to authentication token APNs Connection.
-spec push_notification_token( apns_connection:name()
-spec push_notification_token( apns_connection:name() | pid()
, token()
, device_id()
, json()
, headers()
) -> response().
push_notification_token(ConnectionName, Token, DeviceId, JSONMap, Headers) ->
push_notification_token(ConnectionId, Token, DeviceId, JSONMap, Headers) ->
Notification = jsx:encode(JSONMap),
apns_connection:push_notification( ConnectionName
apns_connection:push_notification( ConnectionId
, Token
, DeviceId
, Notification
Expand Down
40 changes: 22 additions & 18 deletions src/apns_connection.erl
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
%% @doc starts the gen_server
-spec start_link(connection(), pid()) ->
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}.
start_link(#{name := undefined} = Connection, Client) ->
gen_server:start_link(?MODULE, {Connection, Client}, []);
start_link(Connection, Client) ->
Name = name(Connection),
gen_server:start_link({local, Name}, ?MODULE, {Connection, Client}, []).
Expand Down Expand Up @@ -116,35 +118,35 @@ default_connection(token, ConnectionName) ->
}.

%% @doc Close the connection with APNs gracefully
-spec close_connection(name()) -> ok.
close_connection(ConnectionName) ->
gen_server:cast(ConnectionName, stop).
-spec close_connection(name() | pid()) -> ok.
close_connection(ConnectionId) ->
gen_server:cast(ConnectionId, stop).

%% @doc Returns the http2's connection PID. This function is only used in tests.
-spec http2_connection(name()) -> pid().
http2_connection(ConnectionName) ->
gen_server:call(ConnectionName, http2_connection).
-spec http2_connection(name() | pid()) -> pid().
http2_connection(ConnectionId) ->
gen_server:call(ConnectionId, http2_connection).

%% @doc Pushes notification to certificate APNs connection.
-spec push_notification( name()
-spec push_notification( name() | pid()
, apns:device_id()
, notification()
, apns:headers()) -> apns:response().
push_notification(ConnectionName, DeviceId, Notification, Headers) ->
push_notification(ConnectionId, DeviceId, Notification, Headers) ->
{Timeout, StreamId} =
gen_server:call(ConnectionName, {push_notification, DeviceId, Notification, Headers}),
wait_response(ConnectionName, Timeout, StreamId).
gen_server:call(ConnectionId, {push_notification, DeviceId, Notification, Headers}),
wait_response(ConnectionId, Timeout, StreamId).

%% @doc Pushes notification to certificate APNs connection.
-spec push_notification( name()
-spec push_notification( name() | pid()
, apns:token()
, apns:device_id()
, notification()
, apns:headers()) -> apns:response().
push_notification(ConnectionName, Token, DeviceId, Notification, Headers) ->
push_notification(ConnectionId, Token, DeviceId, Notification, Headers) ->
{Timeout, StreamId} =
gen_server:call(ConnectionName, {push_notification, Token, DeviceId, Notification, Headers}),
wait_response(ConnectionName, Timeout, StreamId).
gen_server:call(ConnectionId, {push_notification, Token, DeviceId, Notification, Headers}),
wait_response(ConnectionId, Timeout, StreamId).

%%%===================================================================
%%% gen_server callbacks
Expand Down Expand Up @@ -333,11 +335,13 @@ normalize_response_body([]) ->
normalize_response_body([ResponseBody]) ->
jsx:decode(ResponseBody).

-spec wait_response(name(), integer(), integer()) -> apns:response().
wait_response(ConnectionName, Timeout, StreamID) ->
Server = whereis(ConnectionName),
-spec wait_response(name() | pid(), integer(), integer()) -> apns:response().
wait_response(ConnectionId, Timeout, StreamID) when is_atom(ConnectionId) ->
Server = whereis(ConnectionId),
wait_response(Server, Timeout, StreamID);
wait_response(ConnectionId, Timeout, StreamID) when is_pid(ConnectionId) ->
receive
{apns_response, Server, StreamID, Response} -> Response
{apns_response, ConnectionId, StreamID, Response} -> Response
after
Timeout -> {timeout, StreamID}
end.
Expand Down
42 changes: 35 additions & 7 deletions test/connection_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

-export([ default_connection/1
, connect/1
, connect_without_name/1
, http2_connection_lost/1
, push_notification/1
, push_notification_token/1
Expand All @@ -25,6 +26,7 @@
-spec all() -> [atom()].
all() -> [ default_connection
, connect
, connect_without_name
, http2_connection_lost
, push_notification
, push_notification_token
Expand Down Expand Up @@ -84,12 +86,25 @@ connect(_Config) ->
[_] = meck:unload(),
ok.

-spec connect_without_name(config()) -> ok.
connect_without_name(_Config) ->
ok = mock_open_http2_connection(),
ConnectionName = undefined,
{ok, ServerPid} = apns:connect(cert, ConnectionName),
true = is_process_alive(ServerPid),
ok = close_connection(ServerPid),
[_] = meck:unload(),
ok.

-spec http2_connection_lost(config()) -> ok.
http2_connection_lost(_Config) ->
ok = mock_open_http2_connection(),
ConnectionName = my_connection2,
{ok, ServerPid} = apns:connect(cert, ConnectionName),

HTTP2Conn = apns_connection:http2_connection(ConnectionName),
HTTP2Conn = apns_connection:http2_connection(ServerPid),

true = is_process_alive(HTTP2Conn),
HTTP2Conn ! {crash, ServerPid},
ktn_task:wait_for(fun() -> is_process_alive(HTTP2Conn) end, false),
Expand Down Expand Up @@ -127,7 +142,7 @@ http2_connection_lost(_Config) ->
push_notification(_Config) ->
ok = mock_open_http2_connection(),
ConnectionName = my_connection,
{ok, _ApnsPid} = apns:connect(cert, ConnectionName),
{ok, ServerPid} = apns:connect(cert, ConnectionName),
Headers = #{ apns_id => <<"apnsid">>
, apns_expiration => <<"0">>
, apns_priority => <<"10">>
Expand All @@ -150,8 +165,11 @@ push_notification(_Config) ->

ok = mock_http2_get_response(ErrorCode, ErrorHeaders, ErrorBody),

{ErrorCode, ErrorHeaders, _ErrorBodyDecoded} =
{ErrorCode, ErrorHeaders, ErrorBodyDecoded} =
apns:push_notification(ConnectionName, DeviceId, Notification),
{ErrorCode, ErrorHeaders, ErrorBodyDecoded} =
apns:push_notification(ServerPid, DeviceId, Notification),

ok = close_connection(ConnectionName),
[_] = meck:unload(),
ok.
Expand All @@ -160,7 +178,7 @@ push_notification(_Config) ->
push_notification_token(_Config) ->
ok = mock_open_http2_connection(),
ConnectionName = my_token_connection,
{ok, _ApnsPid} = apns:connect(token, ConnectionName),
{ok, ServerPid} = apns:connect(token, ConnectionName),
Headers = #{ apns_id => <<"apnsid2">>
, apns_expiration => <<"0">>
, apns_priority => <<"10">>
Expand All @@ -182,14 +200,21 @@ push_notification_token(_Config) ->
, Notification
, Headers
),
{ResponseCode, ResponseHeaders, _Body} =
apns:push_notification_token( ServerPid
, Token
, DeviceId
, Notification
, Headers
),
{ResponseCode, ResponseHeaders, no_body} =
apns:push_notification_token( ConnectionName
, Token
, DeviceId
, Notification
),

ok = close_connection(ConnectionName),
ok = close_connection(ServerPid),
_ = meck:unload(),
ok.

Expand Down Expand Up @@ -305,9 +330,12 @@ maybe_mock_apns_os() ->
{0, "12345678"}
end).

close_connection(ConnectionName) ->
ok = apns:close_connection(ConnectionName),
close_connection(ConnectionId) when is_atom(ConnectionId) ->
ConnectionPid = whereis(ConnectionId),
close_connection(ConnectionPid);
close_connection(ConnectionId) when is_pid(ConnectionId) ->
ok = apns:close_connection(ConnectionId),
ktn_task:wait_for(fun() ->
is_process_alive(whereis(ConnectionName))
is_process_alive(ConnectionId)
end, false),
ok.

0 comments on commit 41134c3

Please sign in to comment.