Browse Source

Add the idle_timeout option to HTTP/2

Loïc Hoguin 6 years ago
parent
commit
8185d356c5
3 changed files with 87 additions and 13 deletions
  1. 5 0
      doc/src/manual/cowboy_http2.asciidoc
  2. 28 13
      src/cowboy_http2.erl
  3. 54 0
      test/http2_SUITE.erl

+ 5 - 0
doc/src/manual/cowboy_http2.asciidoc

@@ -20,6 +20,7 @@ opts() :: #{
     connection_type                => worker | supervisor,
     enable_connect_protocol        => boolean(),
     env                            => cowboy_middleware:env(),
+    idle_timeout                   => timeout(),
     inactivity_timeout             => timeout(),
     initial_connection_window_size => 65535..16#7fffffff,
     initial_stream_window_size     => 0..16#7fffffff,
@@ -63,6 +64,10 @@ env (#{})::
 
 Middleware environment.
 
+idle_timeout (60000)::
+
+Time in ms with no data received before Cowboy closes the connection.
+
 inactivity_timeout (300000)::
 
 Time in ms with nothing received at all before Cowboy closes the connection.

+ 28 - 13
src/cowboy_http2.erl

@@ -32,6 +32,7 @@
 	connection_type => worker | supervisor,
 	enable_connect_protocol => boolean(),
 	env => cowboy_middleware:env(),
+	idle_timeout => timeout(),
 	inactivity_timeout => timeout(),
 	initial_connection_window_size => 65535..16#7fffffff,
 	initial_stream_window_size => 0..16#7fffffff,
@@ -64,6 +65,9 @@
 	proxy_header :: undefined | ranch_proxy_header:proxy_info(),
 	opts = #{} :: opts(),
 
+	%% Timer for idle_timeout.
+	timer = undefined :: undefined | reference(),
+
 	%% Remote address and port for the connection.
 	peer = undefined :: {inet:ip_address(), inet:port_number()},
 
@@ -122,13 +126,13 @@ init(Parent, Ref, Socket, Transport, ProxyHeader, Opts) ->
 	binary() | undefined, binary()) -> ok.
 init(Parent, Ref, Socket, Transport, ProxyHeader, Opts, Peer, Sock, Cert, Buffer) ->
 	{ok, Preface, HTTP2Machine} = cow_http2_machine:init(server, Opts),
-	State = #state{parent=Parent, ref=Ref, socket=Socket,
+	State = set_timeout(#state{parent=Parent, ref=Ref, socket=Socket,
 		transport=Transport, proxy_header=ProxyHeader,
 		opts=Opts, peer=Peer, sock=Sock, cert=Cert,
-		http2_init=sequence, http2_machine=HTTP2Machine},
+		http2_init=sequence, http2_machine=HTTP2Machine}),
 	Transport:send(Socket, Preface),
 	case Buffer of
-		<<>> -> before_loop(State, Buffer);
+		<<>> -> loop(State, Buffer);
 		_ -> parse(State, Buffer)
 	end.
 
@@ -154,26 +158,23 @@ init(Parent, Ref, Socket, Transport, ProxyHeader, Opts, Peer, Sock, Cert, Buffer
 		<<"connection">> => <<"Upgrade">>,
 		<<"upgrade">> => <<"h2c">>
 	}, ?MODULE, undefined}), %% @todo undefined or #{}?
-	State = State2#state{http2_init=sequence},
+	State = set_timeout(State2#state{http2_init=sequence}),
 	Transport:send(Socket, Preface),
 	case Buffer of
-		<<>> -> before_loop(State, Buffer);
+		<<>> -> loop(State, Buffer);
 		_ -> parse(State, Buffer)
 	end.
 
-%% @todo Add the timeout for last time since we heard of connection.
-before_loop(State, Buffer) ->
-	loop(State, Buffer).
-
 loop(State=#state{parent=Parent, socket=Socket, transport=Transport,
-		opts=Opts, children=Children}, Buffer) ->
+		opts=Opts, timer=TimerRef, children=Children}, Buffer) ->
+	%% @todo This should only be called when data was read.
 	Transport:setopts(Socket, [{active, once}]),
 	{OK, Closed, Error} = Transport:messages(),
 	InactivityTimeout = maps:get(inactivity_timeout, Opts, 300000),
 	receive
 		%% Socket messages.
 		{OK, Socket, Data} ->
-			parse(State, << Buffer/binary, Data/binary >>);
+			parse(set_timeout(State), << Buffer/binary, Data/binary >>);
 		{Closed, Socket} ->
 			terminate(State, {socket_error, closed, 'The socket has been closed.'});
 		{Error, Socket, Reason} ->
@@ -184,6 +185,9 @@ loop(State=#state{parent=Parent, socket=Socket, transport=Transport,
 		{system, From, Request} ->
 			sys:handle_system_msg(Request, From, Parent, ?MODULE, [], {State, Buffer});
 		%% Timeouts.
+		{timeout, TimerRef, idle_timeout} ->
+			terminate(State, {stop, timeout,
+				'Connection idle longer than configuration allows.'});
 		{timeout, Ref, {shutdown, Pid}} ->
 			cowboy_children:shutdown_timeout(Children, Ref, Pid),
 			loop(State, Buffer);
@@ -206,6 +210,17 @@ loop(State=#state{parent=Parent, socket=Socket, transport=Transport,
 		terminate(State, {internal_error, timeout, 'No message or data received before timeout.'})
 	end.
 
+set_timeout(State=#state{opts=Opts, timer=TimerRef0}) ->
+	ok = case TimerRef0 of
+		undefined -> ok;
+		_ -> erlang:cancel_timer(TimerRef0, [{async, true}, {info, false}])
+	end,
+	TimerRef = case maps:get(idle_timeout, Opts, 60000) of
+		infinity -> undefined;
+		Timeout -> erlang:start_timer(Timeout, self(), idle_timeout)
+	end,
+	State#state{timer=TimerRef}.
+
 %% HTTP/2 protocol parsing.
 
 parse(State=#state{http2_init=sequence}, Data) ->
@@ -213,7 +228,7 @@ parse(State=#state{http2_init=sequence}, Data) ->
 		{ok, Rest} ->
 			parse(State#state{http2_init=settings}, Rest);
 		more ->
-			before_loop(State, Data);
+			loop(State, Data);
 		Error = {connection_error, _, _} ->
 			terminate(State, Error)
 	end;
@@ -229,7 +244,7 @@ parse(State=#state{http2_machine=HTTP2Machine}, Data) ->
 		Error = {connection_error, _, _} ->
 			terminate(State, Error);
 		more ->
-			before_loop(State, Data)
+			loop(State, Data)
 	end.
 
 %% Frames received.

+ 54 - 0
test/http2_SUITE.erl

@@ -51,6 +51,60 @@ do_handshake(Settings, Config) ->
 	{ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000),
 	{ok, Socket}.
 
+idle_timeout(Config) ->
+	doc("Terminate when the idle timeout is reached."),
+	ProtoOpts = #{
+		env => #{dispatch => cowboy_router:compile(init_routes(Config))},
+		idle_timeout => 1000
+	},
+	{ok, _} = cowboy:start_clear(name(), [{port, 0}], ProtoOpts),
+	Port = ranch:get_port(name()),
+	{ok, Socket} = do_handshake([{port, Port}|Config]),
+	timer:sleep(1000),
+	%% Receive a GOAWAY frame back with NO_ERROR.
+	{ok, << _:24, 7:8, _:72, 0:32 >>} = gen_tcp:recv(Socket, 17, 1000),
+	ok.
+
+idle_timeout_infinity(Config) ->
+	doc("Ensure the idle_timeout option accepts the infinity value."),
+	ProtoOpts = #{
+		env => #{dispatch => cowboy_router:compile(init_routes(Config))},
+		idle_timeout => infinity
+	},
+	{ok, _} = cowboy:start_clear(name(), [{port, 0}], ProtoOpts),
+	Port = ranch:get_port(name()),
+	{ok, Socket} = do_handshake([{port, Port}|Config]),
+	timer:sleep(1000),
+	%% Don't receive a GOAWAY frame.
+	{error, timeout} = gen_tcp:recv(Socket, 17, 1000),
+	ok.
+
+idle_timeout_reset_on_data(Config) ->
+	doc("Terminate when the idle timeout is reached."),
+	ProtoOpts = #{
+		env => #{dispatch => cowboy_router:compile(init_routes(Config))},
+		idle_timeout => 1000
+	},
+	{ok, _} = cowboy:start_clear(name(), [{port, 0}], ProtoOpts),
+	Port = ranch:get_port(name()),
+	{ok, Socket} = do_handshake([{port, Port}|Config]),
+	%% We wait a little, send a PING, receive a PING ack.
+	{error, timeout} = gen_tcp:recv(Socket, 17, 500),
+	ok = gen_tcp:send(Socket, cow_http2:ping(0)),
+	{ok, <<8:24, 6:8, 0:7, 1:1, 0:96>>} = gen_tcp:recv(Socket, 17, 1000),
+	%% Again.
+	{error, timeout} = gen_tcp:recv(Socket, 17, 500),
+	ok = gen_tcp:send(Socket, cow_http2:ping(0)),
+	{ok, <<8:24, 6:8, 0:7, 1:1, 0:96>>} = gen_tcp:recv(Socket, 17, 1000),
+	%% And one more time.
+	{error, timeout} = gen_tcp:recv(Socket, 17, 500),
+	ok = gen_tcp:send(Socket, cow_http2:ping(0)),
+	{ok, <<8:24, 6:8, 0:7, 1:1, 0:96>>} = gen_tcp:recv(Socket, 17, 1000),
+	%% The connection goes away soon after we stop sending data.
+	timer:sleep(1000),
+	{ok, << _:24, 7:8, _:72, 0:32 >>} = gen_tcp:recv(Socket, 17, 1000),
+	ok.
+
 inactivity_timeout(Config) ->
 	doc("Terminate when the inactivity timeout is reached."),
 	ProtoOpts = #{