aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2018-11-16 16:30:57 +0100
committerLoïc Hoguin <[email protected]>2018-11-16 16:30:57 +0100
commit8185d356c596e3dda9c87786516c6e924a56617a (patch)
treeb8dfb489729cdecca935f443eddaa5b3e31faa9f
parentf5015cb14bbadef95285460f3d842fbbb05c33c3 (diff)
downloadcowboy-8185d356c596e3dda9c87786516c6e924a56617a.tar.gz
cowboy-8185d356c596e3dda9c87786516c6e924a56617a.tar.bz2
cowboy-8185d356c596e3dda9c87786516c6e924a56617a.zip
Add the idle_timeout option to HTTP/2
-rw-r--r--doc/src/manual/cowboy_http2.asciidoc5
-rw-r--r--src/cowboy_http2.erl41
-rw-r--r--test/http2_SUITE.erl54
3 files changed, 87 insertions, 13 deletions
diff --git a/doc/src/manual/cowboy_http2.asciidoc b/doc/src/manual/cowboy_http2.asciidoc
index 8a79846..2b33a71 100644
--- a/doc/src/manual/cowboy_http2.asciidoc
+++ b/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.
diff --git a/src/cowboy_http2.erl b/src/cowboy_http2.erl
index b83c021..5070bd4 100644
--- a/src/cowboy_http2.erl
+++ b/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.
diff --git a/test/http2_SUITE.erl b/test/http2_SUITE.erl
index ba95173..1fd33f9 100644
--- a/test/http2_SUITE.erl
+++ b/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 = #{