aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/src/manual/gun.ping.asciidoc71
-rw-r--r--src/gun.erl22
-rw-r--r--src/gun_http.erl4
-rw-r--r--src/gun_http2.erl38
-rw-r--r--src/gun_http3.erl6
-rw-r--r--src/gun_raw.erl4
-rw-r--r--src/gun_socks.erl4
-rw-r--r--src/gun_ws.erl4
-rw-r--r--test/rfc7540_SUITE.erl29
9 files changed, 180 insertions, 2 deletions
diff --git a/doc/src/manual/gun.ping.asciidoc b/doc/src/manual/gun.ping.asciidoc
new file mode 100644
index 0000000..96fd8b8
--- /dev/null
+++ b/doc/src/manual/gun.ping.asciidoc
@@ -0,0 +1,71 @@
+= 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 is included in the notification received when
+a ping ack is received from the server.
+
+== Changelog
+
+* *2.2*: Function introduced.
+
+== Examples
+
+.Perform a request
+[source,erlang]
+----
+PingRef = gun:ping(ConnPid).
+receive
+ {gun_notify, ConnPid, ping_ack, PingRef} ->
+ ok
+end.
+----
+
+== See also
+
+link:man:gun(3)[gun(3)],
+link:man:gun:await(3)[gun:await(3)],
diff --git a/src/gun.erl b/src/gun.erl
index 5a3c233..ead6152 100644
--- a/src/gun.erl
+++ b/src/gun.erl
@@ -60,6 +60,10 @@
%% Streaming data.
-export([data/4]).
+%% User pings.
+-export([ping/1]).
+-export([ping/2]).
+
%% Tunneling.
-export([connect/2]).
-export([connect/3]).
@@ -733,6 +737,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().
@@ -1379,6 +1397,10 @@ 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}) ->
+ Commands = Protocol:ping(ProtoState, dereference_stream_ref(StreamRef, State), ReplyTo),
+ commands(Commands, State);
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,
diff --git a/src/gun_http.erl b/src/gun_http.erl
index 98acbc1..d77d9b6 100644
--- a/src/gun_http.erl
+++ b/src/gun_http.erl
@@ -26,6 +26,7 @@
-export([closing/4]).
-export([close/4]).
-export([keepalive/3]).
+-export([ping/3]).
-export([headers/12]).
-export([request/13]).
-export([data/7]).
@@ -560,6 +561,9 @@ keepalive(#http_state{socket=Socket, transport=Transport, out=head}, _, EvHandle
keepalive(_State, _, EvHandlerState) ->
{[], EvHandlerState}.
+ping(_State, _PingRef, _ReplyTo) ->
+ {error, unsupported_by_protocol}.
+
headers(State, StreamRef, ReplyTo, _, _, _, _, _, _, CookieStore, _, EvHandlerState)
when is_list(StreamRef) ->
gun:reply(ReplyTo, {gun_error, self(), stream_ref(State, StreamRef),
diff --git a/src/gun_http2.erl b/src/gun_http2.erl
index 15010f5..bc100f5 100644
--- a/src/gun_http2.erl
+++ b/src/gun_http2.erl
@@ -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]).
@@ -82,6 +83,12 @@
tunnel :: undefined | #tunnel{}
}).
+-record(user_ping, {
+ ref :: reference(),
+ reply_to :: pid(),
+ payload :: integer()
+}).
+
-record(http2_state, {
reply_to :: gun:reply_to(),
socket :: inet:socket() | ssl:sslsocket(),
@@ -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.
@@ -353,7 +363,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.
@@ -373,8 +383,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=PingReplyTo}, UserPings} ->
+ RealStreamRef = stream_ref(State, StreamRef),
+ PingReplyTo ! {gun_notify, self(), ping_ack, 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.
@@ -948,6 +971,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 = erlang:unique_integer([monotonic, positive]),
+ 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, _} ->
+ {ok, Error}
+ end.
+
headers(State, StreamRef, ReplyTo, Method, Host, Port, Path,
Headers, InitialFlow, CookieStore, EvHandler, EvHandlerState) ->
request_common(State, StreamRef, ReplyTo, CookieStore, EvHandler, EvHandlerState,
diff --git a/src/gun_http3.erl b/src/gun_http3.erl
index 49930c7..5b95d4a 100644
--- a/src/gun_http3.erl
+++ b/src/gun_http3.erl
@@ -28,6 +28,7 @@
-export([closing/4]).
-export([close/4]).
-export([keepalive/3]).
+-export([ping/3]).
-export([headers/12]).
-export([request/13]).
-export([data/7]).
@@ -486,6 +487,11 @@ close(_Reason, _State, _, EvHandlerState) ->
keepalive(_State, _, _EvHandlerState) ->
error(todo).
+-spec ping(_, _, _) -> {error, not_implemented}.
+
+ping(_State, _PingRef, _ReplyTo) ->
+ {error, not_implemented}.
+
headers(State0=#http3_state{conn=Conn, transport=Transport,
http3_machine=HTTP3Machine0}, StreamRef, ReplyTo, Method, Host, Port,
Path, Headers0, _InitialFlow0, CookieStore0, EvHandler, EvHandlerState0)
diff --git a/src/gun_raw.erl b/src/gun_raw.erl
index a3f1ef5..3c506b6 100644
--- a/src/gun_raw.erl
+++ b/src/gun_raw.erl
@@ -23,6 +23,7 @@
-export([update_flow/4]).
-export([closing/4]).
-export([close/4]).
+-export([ping/3]).
-export([data/7]).
-export([down/1]).
@@ -84,6 +85,9 @@ closing(_, _, _, EvHandlerState) ->
close(_, _, _, EvHandlerState) ->
EvHandlerState.
+ping(_State, _PingRef, _ReplyTo) ->
+ {error, unsupported_by_protocol}.
+
%% @todo Initiate closing on IsFin=fin.
data(#raw_state{ref=StreamRef, socket=Socket, transport=Transport}, StreamRef,
_ReplyTo, _IsFin, Data, _EvHandler, EvHandlerState) ->
diff --git a/src/gun_socks.erl b/src/gun_socks.erl
index d29f906..7a2bdd7 100644
--- a/src/gun_socks.erl
+++ b/src/gun_socks.erl
@@ -23,6 +23,7 @@
-export([handle/5]).
-export([closing/4]).
-export([close/4]).
+-export([ping/3]).
%% @todo down
-record(socks_state, {
@@ -205,3 +206,6 @@ closing(_, _, _, EvHandlerState) ->
close(_, _, _, EvHandlerState) ->
EvHandlerState.
+
+ping(_State, _PingRef, _ReplyTo) ->
+ {error, unsupported_by_protocol}.
diff --git a/src/gun_ws.erl b/src/gun_ws.erl
index dfe1139..09e29aa 100644
--- a/src/gun_ws.erl
+++ b/src/gun_ws.erl
@@ -28,6 +28,7 @@
-export([closing/4]).
-export([close/4]).
-export([keepalive/3]).
+-export([ping/3]).
-export([ws_send/6]).
-export([down/1]).
@@ -307,6 +308,9 @@ close(_, _, _, EvHandlerState) ->
keepalive(State=#ws_state{reply_to=ReplyTo}, EvHandler, EvHandlerState0) ->
send(ping, State, ReplyTo, EvHandler, EvHandlerState0).
+ping(_State, _PingRef, _ReplyTo) ->
+ {error, not_implemented}.
+
%% Send one frame.
send(Frame, State=#ws_state{stream_ref=StreamRef,
socket=Socket, transport=Transport, in=In, extensions=Extensions},
diff --git a/test/rfc7540_SUITE.erl b/test/rfc7540_SUITE.erl
index 5bfa6cd..b629ec0 100644
--- a/test/rfc7540_SUITE.erl
+++ b/test/rfc7540_SUITE.erl
@@ -534,6 +534,35 @@ 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),
+ receive
+ {gun_notify, Pid, ping_ack, PingRef} ->
+ ok
+ after 1000 ->
+ error(timeout)
+ end,
+ gun:close(Pid).
+
do_ping_ack_loop_fun() ->
%% Receive ping, sync with parent, send ping ack, loop.
fun Loop(Parent, ListenSocket, Socket, Transport) ->