aboutsummaryrefslogtreecommitdiffstats
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/handlers/delay_hello_h.erl10
-rw-r--r--test/http2_SUITE.erl132
-rw-r--r--test/http_SUITE.erl71
-rw-r--r--test/rfc7540_SUITE.erl85
4 files changed, 268 insertions, 30 deletions
diff --git a/test/handlers/delay_hello_h.erl b/test/handlers/delay_hello_h.erl
index 7e59be6..ee3ee9c 100644
--- a/test/handlers/delay_hello_h.erl
+++ b/test/handlers/delay_hello_h.erl
@@ -4,6 +4,14 @@
-export([init/2]).
-init(Req, Delay) ->
+init(Req, Delay) when is_integer(Delay) ->
+ init(Req, #{delay => Delay});
+init(Req, Opts=#{delay := Delay}) ->
+ _ = case Opts of
+ #{notify_received := Pid} ->
+ Pid ! {request_received, maps:get(path, Req)};
+ _ ->
+ ok
+ end,
timer:sleep(Delay),
{ok, cowboy_req:reply(200, #{}, <<"Hello world!">>, Req), Delay}.
diff --git a/test/http2_SUITE.erl b/test/http2_SUITE.erl
index 44fc5cc..fe6325d 100644
--- a/test/http2_SUITE.erl
+++ b/test/http2_SUITE.erl
@@ -284,3 +284,135 @@ settings_timeout_infinity(Config) ->
after
cowboy:stop_listener(?FUNCTION_NAME)
end.
+
+graceful_shutdown_connection(Config) ->
+ doc("Check that ongoing requests are handled before gracefully shutting down a connection."),
+ Dispatch = cowboy_router:compile([{"localhost", [
+ {"/delay_hello", delay_hello_h,
+ #{delay => 500, notify_received => self()}}
+ ]}]),
+ ProtoOpts = #{
+ env => #{dispatch => Dispatch}
+ },
+ {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], ProtoOpts),
+ Port = ranch:get_port(?FUNCTION_NAME),
+ try
+ ConnPid = gun_open([{type, tcp}, {protocol, http2}, {port, Port}|Config]),
+ Ref = gun:get(ConnPid, "/delay_hello"),
+ %% Make sure the request is received.
+ receive {request_received, <<"/delay_hello">>} -> ok end,
+ %% Tell the connection to shutdown while the handler is working.
+ [CowboyConnPid] = ranch:procs(?FUNCTION_NAME, connections),
+ monitor(process, CowboyConnPid),
+ ok = sys:terminate(CowboyConnPid, goaway),
+ %% Check that the response is sent to the client before the
+ %% connection goes down.
+ {response, nofin, 200, _RespHeaders} = gun:await(ConnPid, Ref),
+ {ok, RespBody} = gun:await_body(ConnPid, Ref),
+ <<"Hello world!">> = iolist_to_binary(RespBody),
+ %% Check that the connection is gone soon afterwards. (The exit
+ %% reason is supposed to be 'goaway' as passed to
+ %% sys:terminate/2, but it is {shutdown, closed}.)
+ receive
+ {'DOWN', _, process, CowboyConnPid, _Reason} ->
+ ok
+ end,
+ [] = ranch:procs(?FUNCTION_NAME, connections),
+ gun:close(ConnPid)
+ after
+ cowboy:stop_listener(?FUNCTION_NAME)
+ end.
+
+graceful_shutdown_timeout(Config) ->
+ doc("Check that a connection is closed when gracefully shutting down times out."),
+ Dispatch = cowboy_router:compile([{"localhost", [
+ {"/long_delay_hello", delay_hello_h,
+ #{delay => 10000, notify_received => self()}}
+ ]}]),
+ ProtoOpts = #{
+ env => #{dispatch => Dispatch},
+ goaway_initial_timeout => 200,
+ goaway_complete_timeout => 500
+ },
+ {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], ProtoOpts),
+ Port = ranch:get_port(?FUNCTION_NAME),
+ try
+ ConnPid = gun_open([{type, tcp}, {protocol, http2}, {port, Port}|Config]),
+ Ref = gun:get(ConnPid, "/long_delay_hello"),
+ %% Make sure the request is received.
+ receive {request_received, <<"/long_delay_hello">>} -> ok end,
+ %% Tell the connection to shutdown while the handler is working.
+ [CowboyConnPid] = ranch:procs(?FUNCTION_NAME, connections),
+ monitor(process, CowboyConnPid),
+ ok = sys:terminate(CowboyConnPid, goaway),
+ %% Check that connection didn't wait for the slow handler.
+ {error, {stream_error, closed}} = gun:await(ConnPid, Ref),
+ %% Check that the connection is gone. (The exit reason is
+ %% supposed to be 'goaway' as passed to sys:terminate/2, but it
+ %% is {shutdown, {stop, {exit, goaway}, 'Graceful shutdown timed
+ %% out.'}}.)
+ receive
+ {'DOWN', _, process, CowboyConnPid, _Reason} ->
+ ok
+ after 100 ->
+ error(still_alive)
+ end,
+ [] = ranch:procs(?FUNCTION_NAME, connections),
+ gun:close(ConnPid)
+ after
+ cowboy:stop_listener(?FUNCTION_NAME)
+ end.
+
+graceful_shutdown_listener(Config) ->
+ doc("Check that connections are shut down gracefully when stopping a listener."),
+ Dispatch = cowboy_router:compile([{"localhost", [
+ {"/delay_hello", delay_hello_h,
+ #{delay => 500, notify_received => self()}}
+ ]}]),
+ ProtoOpts = #{
+ env => #{dispatch => Dispatch}
+ },
+ {ok, Listener} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], ProtoOpts),
+ Port = ranch:get_port(?FUNCTION_NAME),
+ ConnPid = gun_open([{type, tcp}, {protocol, http2}, {port, Port}|Config]),
+ Ref = gun:get(ConnPid, "/delay_hello"),
+ %% Shutdown listener while the handlers are working.
+ receive {request_received, <<"/delay_hello">>} -> ok end,
+ ListenerMonitorRef = monitor(process, Listener),
+ ok = cowboy:stop_listener(?FUNCTION_NAME),
+ receive
+ {'DOWN', ListenerMonitorRef, process, Listener, _Reason} ->
+ ok
+ end,
+ %% Check that the request is handled before shutting down.
+ {response, nofin, 200, _RespHeaders} = gun:await(ConnPid, Ref),
+ {ok, RespBody} = gun:await_body(ConnPid, Ref),
+ <<"Hello world!">> = iolist_to_binary(RespBody),
+ gun:close(ConnPid).
+
+graceful_shutdown_listener_timeout(Config) ->
+ doc("Check that connections are shut down when gracefully stopping a listener times out."),
+ Dispatch = cowboy_router:compile([{"localhost", [
+ {"/long_delay_hello", delay_hello_h,
+ #{delay => 10000, notify_received => self()}}
+ ]}]),
+ ProtoOpts = #{
+ env => #{dispatch => Dispatch},
+ goaway_initial_timeout => 200,
+ goaway_complete_timeout => 500
+ },
+ {ok, Listener} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], ProtoOpts),
+ Port = ranch:get_port(?FUNCTION_NAME),
+ ConnPid = gun_open([{type, tcp}, {protocol, http2}, {port, Port}|Config]),
+ Ref = gun:get(ConnPid, "/long_delay_hello"),
+ %% Shutdown listener while the handlers are working.
+ receive {request_received, <<"/long_delay_hello">>} -> ok end,
+ ListenerMonitorRef = monitor(process, Listener),
+ ok = cowboy:stop_listener(?FUNCTION_NAME),
+ receive
+ {'DOWN', ListenerMonitorRef, process, Listener, _Reason} ->
+ ok
+ end,
+ %% Check that the slow request is aborted.
+ {error, {stream_error, closed}} = gun:await(ConnPid, Ref),
+ gun:close(ConnPid).
diff --git a/test/http_SUITE.erl b/test/http_SUITE.erl
index 0b4edd9..d0c92e4 100644
--- a/test/http_SUITE.erl
+++ b/test/http_SUITE.erl
@@ -20,6 +20,7 @@
-import(ct_helper, [doc/1]).
-import(ct_helper, [get_remote_pid_tcp/1]).
-import(cowboy_test, [gun_open/1]).
+-import(cowboy_test, [gun_down/1]).
-import(cowboy_test, [raw_open/1]).
-import(cowboy_test, [raw_send/2]).
-import(cowboy_test, [raw_recv_head/1]).
@@ -443,3 +444,73 @@ switch_protocol_flush(Config) ->
after
cowboy:stop_listener(?FUNCTION_NAME)
end.
+
+graceful_shutdown_connection(Config) ->
+ doc("Check that the current request is handled before gracefully "
+ "shutting down a connection."),
+ Dispatch = cowboy_router:compile([{"localhost", [
+ {"/delay_hello", delay_hello_h,
+ #{delay => 500, notify_received => self()}},
+ {"/long_delay_hello", delay_hello_h,
+ #{delay => 10000, notify_received => self()}}
+ ]}]),
+ ProtoOpts = #{
+ env => #{dispatch => Dispatch}
+ },
+ {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], ProtoOpts),
+ Port = ranch:get_port(?FUNCTION_NAME),
+ try
+ ConnPid = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]),
+ {ok, http} = gun:await_up(ConnPid),
+ #{socket := Socket} = gun:info(ConnPid),
+ CowboyConnPid = get_remote_pid_tcp(Socket),
+ CowboyConnRef = erlang:monitor(process, CowboyConnPid),
+ Ref1 = gun:get(ConnPid, "/delay_hello"),
+ Ref2 = gun:get(ConnPid, "/delay_hello"),
+ receive {request_received, <<"/delay_hello">>} -> ok end,
+ receive {request_received, <<"/delay_hello">>} -> ok end,
+ ok = sys:terminate(CowboyConnPid, system_is_going_down),
+ {response, nofin, 200, RespHeaders} = gun:await(ConnPid, Ref1),
+ <<"close">> = proplists:get_value(<<"connection">>, RespHeaders),
+ {ok, RespBody} = gun:await_body(ConnPid, Ref1),
+ <<"Hello world!">> = iolist_to_binary(RespBody),
+ {error, {stream_error, _}} = gun:await(ConnPid, Ref2),
+ ok = gun_down(ConnPid),
+ receive
+ {'DOWN', CowboyConnRef, process, CowboyConnPid, _Reason} ->
+ ok
+ end
+ after
+ cowboy:stop_listener(?FUNCTION_NAME)
+ end.
+
+graceful_shutdown_listener(Config) ->
+ doc("Check that connections are shut down gracefully when stopping a listener."),
+ Dispatch = cowboy_router:compile([{"localhost", [
+ {"/delay_hello", delay_hello_h,
+ #{delay => 500, notify_received => self()}},
+ {"/long_delay_hello", delay_hello_h,
+ #{delay => 10000, notify_received => self()}}
+ ]}]),
+ ProtoOpts = #{
+ env => #{dispatch => Dispatch}
+ },
+ {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], ProtoOpts),
+ Port = ranch:get_port(?FUNCTION_NAME),
+ ConnPid1 = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]),
+ Ref1 = gun:get(ConnPid1, "/delay_hello"),
+ ConnPid2 = gun_open([{type, tcp}, {protocol, http}, {port, Port}|Config]),
+ Ref2 = gun:get(ConnPid2, "/long_delay_hello"),
+ %% Shutdown listener while the handlers are working.
+ receive {request_received, <<"/delay_hello">>} -> ok end,
+ receive {request_received, <<"/long_delay_hello">>} -> ok end,
+ ok = cowboy:stop_listener(?FUNCTION_NAME),
+ %% Check that the 1st request is handled before shutting down.
+ {response, nofin, 200, RespHeaders} = gun:await(ConnPid1, Ref1),
+ <<"close">> = proplists:get_value(<<"connection">>, RespHeaders),
+ {ok, RespBody} = gun:await_body(ConnPid1, Ref1),
+ <<"Hello world!">> = iolist_to_binary(RespBody),
+ gun:close(ConnPid1),
+ %% Check that the 2nd (very slow) request is not handled.
+ {error, {stream_error, closed}} = gun:await(ConnPid2, Ref2),
+ gun:close(ConnPid2).
diff --git a/test/rfc7540_SUITE.erl b/test/rfc7540_SUITE.erl
index aec0aa1..6d8aa91 100644
--- a/test/rfc7540_SUITE.erl
+++ b/test/rfc7540_SUITE.erl
@@ -18,6 +18,7 @@
-import(ct_helper, [config/2]).
-import(ct_helper, [doc/1]).
+-import(ct_helper, [get_remote_pid_tcp/1]).
-import(cowboy_test, [gun_open/1]).
-import(cowboy_test, [raw_open/1]).
-import(cowboy_test, [raw_send/2]).
@@ -52,6 +53,7 @@ init_routes(_) -> [
{"localhost", [
{"/", hello_h, []},
{"/echo/:key", echo_h, []},
+ {"/delay_hello", delay_hello_h, 1200},
{"/long_polling", long_polling_h, []},
{"/loop_handler_abort", loop_handler_abort_h, []},
{"/resp/:key[/:arg]", resp_h, []}
@@ -2955,39 +2957,64 @@ client_settings_disable_push(Config) ->
%% (RFC7540 6.8) GOAWAY
% @todo GOAWAY frames have a reserved bit in the payload that must be ignored.
%
-%% @todo We should eventually implement the mechanism for gracefully
-%% shutting down of the connection. (Send the GOAWAY, finish processing
-%% the current set of streams, give up after a certain timeout.)
-%
-%% @todo If we graceful shutdown and receive a GOAWAY, we give up too.
% A GOAWAY frame might not immediately precede closing of the
% connection; a receiver of a GOAWAY that has no more use for the
% connection SHOULD still send a GOAWAY frame before terminating the
% connection.
-%
-%% @todo And it gets more complex when you think about h1 to h2 proxies.
-% A server that is attempting to gracefully shut down a
-% connection SHOULD send an initial GOAWAY frame with the last stream
-% identifier set to 2^31-1 and a NO_ERROR code. This signals to the
-% client that a shutdown is imminent and that initiating further
-% requests is prohibited. After allowing time for any in-flight stream
-% creation (at least one round-trip time), the server can send another
-% GOAWAY frame with an updated last stream identifier. This ensures
-% that a connection can be cleanly shut down without losing requests.
-%
-%% @todo And of course even if we shutdown we need to be careful about
-%% the connection state.
-% After sending a GOAWAY frame, the sender can discard frames for
-% streams initiated by the receiver with identifiers higher than the
-% identified last stream. However, any frames that alter connection
-% state cannot be completely ignored. For instance, HEADERS,
-% PUSH_PROMISE, and CONTINUATION frames MUST be minimally processed to
-% ensure the state maintained for header compression is consistent (see
-% Section 4.3); similarly, DATA frames MUST be counted toward the
-% connection flow-control window. Failure to process these frames can
-% cause flow control or header compression state to become
-% unsynchronized.
-%
+
+graceful_shutdown_client_stays(Config) ->
+ doc("A server gracefully shutting down must send a GOAWAY frame with the "
+ "last stream identifier set to 2^31-1 and a NO_ERROR code. After allowing "
+ "time for any in-flight stream creation the server can send another GOAWAY "
+ "frame with an updated last stream identifier. (RFC7540 6.8)"),
+ {ok, Socket} = do_handshake(Config),
+ ServerConnPid = get_remote_pid_tcp(Socket),
+ ok = sys:terminate(ServerConnPid, whatever),
+ %% First GOAWAY frame.
+ {ok, <<_:24, 7:8, 0:8, 0:1, 0:31, 0:1, 16#7fffffff:31, 0:32>>} = gen_tcp:recv(Socket, 17, 500),
+ %% Second GOAWAY frame.
+ {ok, <<_:24, 7:8, 0:8, 0:1, 0:31, 0:1, 0:31, 0:32>>} = gen_tcp:recv(Socket, 17, 1500),
+ {error, closed} = gen_tcp:recv(Socket, 3, 1000),
+ ok.
+
+%% @todo We should add this test also for discarded DATA and CONTINUATION frames.
+%% The test can be the same for CONTINUATION (just send headers differently) but
+%% the DATA test should make sure the global window is not corrupted.
+%%
+%% @todo We should extend this test to have two requests: one initiated before
+%% the second GOAWAY, but not terminated; another initiated after the GOAWAY, terminated.
+%% Finally the first request is terminated by sending a body and a trailing
+%% HEADERS frame. This way we know for sure that the connection state is not corrupt.
+graceful_shutdown_race_condition(Config) ->
+ doc("A server in the process of gracefully shutting down must discard frames "
+ "for streams initiated by the receiver with identifiers higher than the "
+ "identified last stream. This may include frames that alter connection "
+ "state such as HEADERS frames. (RFC7540 6.8)"),
+ {ok, Socket} = do_handshake(Config),
+ ServerConnPid = get_remote_pid_tcp(Socket),
+ ok = sys:terminate(ServerConnPid, whatever),
+ %% First GOAWAY frame.
+ {ok, <<_:24, 7:8, 0:8, 0:1, 0:31, 0:1, 16#7fffffff:31, 0:32>>} = gen_tcp:recv(Socket, 17, 500),
+ %% Simulate an in-flight request, sent by the client before the
+ %% GOAWAY frame arrived to the client.
+ {HeadersBlock, _} = cow_hpack:encode([
+ {<<":method">>, <<"GET">>},
+ {<<":scheme">>, <<"http">>},
+ {<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+ {<<":path">>, <<"/delay_hello">>}
+ ]),
+ ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)),
+ %% Second GOAWAY frame.
+ {ok, <<_:24, 7:8, 0:8, 0:1, 0:31, 0:1, 1:31, 0:32>>} = gen_tcp:recv(Socket, 17, 2000),
+ %% The client tries to send another request, ignoring the GOAWAY.
+ ok = gen_tcp:send(Socket, cow_http2:headers(3, fin, HeadersBlock)),
+ %% The server responds to the first request (streamid 1) and closes.
+ {ok, <<RespHeadersPayloadLength:24, 1, 4, 0:1, 1:31>>} = gen_tcp:recv(Socket, 9, 1000),
+ {ok, _RespHeaders} = gen_tcp:recv(Socket, RespHeadersPayloadLength, 1000),
+ {ok, <<12:24, 0, 1, 0:1, 1:31, "Hello world!">>} = gen_tcp:recv(Socket, 21, 1000),
+ {error, closed} = gen_tcp:recv(Socket, 3, 1000),
+ ok.
+
% The GOAWAY frame applies to the connection, not a specific stream.
% An endpoint MUST treat a GOAWAY frame with a stream identifier other
% than 0x0 as a connection error (Section 5.4.1) of type