diff options
-rw-r--r-- | doc/src/manual/gun.ping.asciidoc | 71 | ||||
-rw-r--r-- | src/gun.erl | 22 | ||||
-rw-r--r-- | src/gun_http.erl | 4 | ||||
-rw-r--r-- | src/gun_http2.erl | 38 | ||||
-rw-r--r-- | src/gun_http3.erl | 6 | ||||
-rw-r--r-- | src/gun_raw.erl | 4 | ||||
-rw-r--r-- | src/gun_socks.erl | 4 | ||||
-rw-r--r-- | src/gun_ws.erl | 4 | ||||
-rw-r--r-- | test/rfc7540_SUITE.erl | 29 |
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) -> |