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

Add gun:ping/2,3 for user-initiated ping for HTTP/2 #343

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
68 changes: 68 additions & 0 deletions doc/src/manual/gun.ping.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
= gun:ping(3)

== Name

gun:ping - Check the health or the round-trip time of a connection
without sending a request.

== Description

[source,erlang]
----
ping(ConnPid)
-> ping(ConnPid, #{})

ping(ConnPid, ReqOpts)
-> PingRef

ConnPid :: pid()
ReqOpts :: gun:req_opts()
PingRef :: gun:stream_ref()
----

Send a ping.

A ping can be sent to check the health or to measure the
round-trip time of a connection, without sending a request.

The function `ping/1,2` sends a ping immediately, if the
protocol supports pings. The server responds with a ping ack.
A call to `gun:await/2,3` returns `ping_ack` when the ping
ack has been received from the server.

Currently, explicit ping is supported only for HTTP/2.

== Arguments

ConnPid::

The pid of the Gun connection process.

ReqOpts::

Request options. Only the `reply_to` and `tunnel` options
can be used.

== Return value

A reference that identifies the ping is returned. This
reference must be passed in subsequent calls and will be
received in messages related to this ping.

== Changelog

* *2.x*: Function introduced.

== Examples

.Perform a request
[source,erlang]
----
PingRef = gun:ping(ConnPid).
ping_ack = gun:await(ConnPid, PingRef).
----

== See also

link:man:gun(3)[gun(3)],
link:man:gun:await(3)[gun:await(3)],
29 changes: 29 additions & 0 deletions src/gun.erl
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@
%% Streaming data.
-export([data/4]).

%% User pings.
-export([ping/1]).
-export([ping/2]).

%% Tunneling.
-export([connect/2]).
-export([connect/3]).
Expand Down Expand Up @@ -721,6 +725,20 @@ data(ServerPid, StreamRef, IsFin, Data) ->
gen_statem:cast(ServerPid, {data, self(), StreamRef, IsFin, Data})
end.

%% User pings.

-spec ping(pid()) -> stream_ref().
ping(ServerPid) ->
ping(ServerPid, #{}).

-spec ping(pid(), req_opts()) -> stream_ref().
ping(ServerPid, ReqOpts) ->
Tunnel = get_tunnel(ReqOpts),
StreamRef = make_stream_ref(Tunnel),
ReplyTo = maps:get(reply_to, ReqOpts, self()),
gen_statem:cast(ServerPid, {ping, ReplyTo, StreamRef}),
StreamRef.

%% Tunneling.

-spec connect(pid(), connect_destination()) -> stream_ref().
Expand Down Expand Up @@ -782,6 +800,8 @@ await(ServerPid, StreamRef, Timeout, MRef) ->
{up, Protocol};
{gun_notify, ServerPid, Type, Info} ->
{notify, Type, Info};
{gun_ping_ack, ServerPid, StreamRef} ->
ping_ack;
{gun_error, ServerPid, StreamRef, Reason} ->
{error, {stream_error, Reason}};
{gun_error, ServerPid, Reason} ->
Expand Down Expand Up @@ -1367,6 +1387,15 @@ connected_ws_only(Type, Event, State) ->
%%
%% @todo It might be better, internally, to pass around a URIMap
%% containing the target URI, instead of separate Host/Port/PathWithQs.
connected(cast, {ping, ReplyTo, StreamRef},
State=#state{protocol=Protocol, protocol_state=ProtoState}) ->
case erlang:function_exported(Protocol, ping, 3) of
true ->
Commands = Protocol:ping(ProtoState, dereference_stream_ref(StreamRef, State), ReplyTo),
commands(Commands, State);
false ->
ReplyTo ! {gun_error, self(), StreamRef, not_supported_for_protocol}
end;
connected(cast, {headers, ReplyTo, StreamRef, Method, Path, Headers, InitialFlow},
State=#state{origin_host=Host, origin_port=Port,
protocol=Protocol, protocol_state=ProtoState, cookie_store=CookieStore0,
Expand Down
38 changes: 36 additions & 2 deletions src/gun_http2.erl
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
-export([closing/4]).
-export([close/4]).
-export([keepalive/3]).
-export([ping/3]).
-export([headers/12]).
-export([request/13]).
-export([data/7]).
Expand Down Expand Up @@ -82,6 +83,12 @@
tunnel :: undefined | #tunnel{}
}).

-record(user_ping, {
ref :: reference(),
reply_to :: pid(),
payload :: integer()
}).

-record(http2_state, {
reply_to :: pid(),
socket :: inet:socket() | ssl:sslsocket(),
Expand Down Expand Up @@ -115,6 +122,9 @@
streams = #{} :: #{cow_http2:streamid() => #stream{}},
stream_refs = #{} :: #{reference() => cow_http2:streamid()},

%% User-initiated pings that have not yet been acknowledged.
user_pings = [] :: [#user_ping{}],

%% Number of pings that have been sent but not yet acknowledged.
%% Used to determine whether the connection should be closed when
%% the keepalive_tolerance option is set.
Expand Down Expand Up @@ -351,7 +361,7 @@ frame(State=#http2_state{http2_machine=HTTP2Machine0}, Frame, CookieStore, EvHan

maybe_ack_or_notify(State=#http2_state{reply_to=ReplyTo, socket=Socket,
transport=Transport, opts=Opts, http2_machine=HTTP2Machine,
pings_unack=PingsUnack}, Frame) ->
pings_unack=PingsUnack, user_pings=UserPings0}, Frame) ->
case Frame of
{settings, _} ->
%% We notify remote settings changes only if the user requested it.
Expand All @@ -371,8 +381,21 @@ maybe_ack_or_notify(State=#http2_state{reply_to=ReplyTo, socket=Socket,
ok -> {state, State};
Error={error, _} -> Error
end;
{ping_ack, _Opaque} ->
{ping_ack, 0} ->
%% Internal ping payload used for keepalive.
{state, State#http2_state{pings_unack=PingsUnack - 1}};
{ping_ack, Payload} ->
%% User ping.
case lists:keytake(Payload, #user_ping.payload, UserPings0) of
{value, #user_ping{ref=StreamRef, reply_to=ReplyTo}, UserPings} ->
RealStreamRef = stream_ref(State, StreamRef),
ReplyTo ! {gun_ping_ack, self(), RealStreamRef},
{state, State#http2_state{user_pings=UserPings}};
false ->
%% Ignore unexpected ping ack. RFC 7540
%% doesn't explicitly forbid it.
{state, State}
end;
_ ->
{state, State}
end.
Expand Down Expand Up @@ -934,6 +957,17 @@ keepalive(State=#http2_state{socket=Socket, transport=Transport, pings_unack=Pin
{Error, EvHandlerState}
end.

ping(State=#http2_state{socket=Socket, transport=Transport, user_pings=UserPings}, StreamRef, ReplyTo) ->
%% Use non-zero 64-bit payload for user pings. 0 is reserved for keepalive.
Payload = rand:uniform(16#ffffffffffffffff),
case Transport:send(Socket, cow_http2:ping(Payload)) of
ok ->
UserPing = #user_ping{ref = StreamRef, reply_to = ReplyTo, payload = Payload},
{state, State#http2_state{user_pings = UserPings ++ [UserPing]}};
Error={error, _} ->
Error
end.

headers(State=#http2_state{socket=Socket, transport=Transport, opts=Opts,
http2_machine=HTTP2Machine0}, StreamRef, ReplyTo, Method, Host, Port,
Path, Headers0, InitialFlow0, CookieStore0, EvHandler, EvHandlerState0)
Expand Down
24 changes: 24 additions & 0 deletions test/rfc7540_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,30 @@ keepalive_tolerance_ping_ack_timeout(_) ->
error(timeout)
end.

user_initiated_ping(_) ->
doc("The PING frame allows a client to safely test whether a connection is"
" still active without sending a request. (RFC7540 8.1.4)"),
{ok, OriginPid, OriginPort} = init_origin(tcp, http2, fun (_, _, Socket, Transport) ->
{ok, Data} = Transport:recv(Socket, 9, infinity),
<<Len:24, 6:8, %% PING
0:8, %% Flags
0:1, 0:31>> = Data,
{ok, Payload} = Transport:recv(Socket, Len, 1000),
8 = Len = byte_size(Payload),
Ack = <<8:24, 6:8, %% PING
1:8, %% Ack flag
0:1, 0:31, Payload/binary>>,
ok = Transport:send(Socket, Ack)
end),
{ok, Pid} = gun:open("localhost", OriginPort, #{
protocols => [http2]
}),
{ok, http2} = gun:await_up(Pid),
handshake_completed = receive_from(OriginPid),
PingRef = gun:ping(Pid),
ping_ack = gun:await(Pid, PingRef, 1000),
gun:close(Pid).

do_ping_ack_loop_fun() ->
%% Receive ping, sync with parent, send ping ack, loop.
fun Loop(Parent, ListenSocket, Socket, Transport) ->
Expand Down
Loading