Skip to content
This repository
Browse code

Add a test for websocket hibernate + timeout and fix this use case

The issue was that we were calling erlang:hibernate before a
receive .. after .. end call. Erlang hibernates the process before
reaching the receive instruction and we therefore couldn't enter
the after clause when hibernating.

This is now fixed by using erlang:send_after instead and receiving
that message instead of using an after clause.
  • Loading branch information...
commit d0f711a61d54e3286b71017d20a9cc8fe1eff7ed 1 parent 04f55eb
authored September 22, 2011
25  src/cowboy_http_websocket.erl
@@ -52,6 +52,7 @@
52 52
 	opts :: any(),
53 53
 	challenge = undefined :: undefined | binary(),
54 54
 	timeout = infinity :: timeout(),
  55
+	timeout_ref = undefined :: undefined | reference(),
55 56
 	messages = undefined :: undefined | {atom(), atom(), atom()},
56 57
 	hibernate = false :: boolean(),
57 58
 	eop :: undefined | tuple(), %% hixie-76 specific.
@@ -170,16 +171,28 @@ handler_before_loop(State=#state{hibernate=true},
170 171
 		Req=#http_req{socket=Socket, transport=Transport},
171 172
 		HandlerState, SoFar) ->
172 173
 	Transport:setopts(Socket, [{active, once}]),
173  
-	erlang:hibernate(?MODULE, handler_loop, [State#state{hibernate=false},
  174
+	State2 = handler_loop_timeout(State),
  175
+	erlang:hibernate(?MODULE, handler_loop, [State2#state{hibernate=false},
174 176
 		Req, HandlerState, SoFar]);
175 177
 handler_before_loop(State, Req=#http_req{socket=Socket, transport=Transport},
176 178
 		HandlerState, SoFar) ->
177 179
 	Transport:setopts(Socket, [{active, once}]),
178  
-	handler_loop(State, Req, HandlerState, SoFar).
  180
+	State2 = handler_loop_timeout(State),
  181
+	handler_loop(State2, Req, HandlerState, SoFar).
  182
+
  183
+-spec handler_loop_timeout(#state{}) -> #state{}.
  184
+handler_loop_timeout(State=#state{timeout=infinity}) ->
  185
+	State#state{timeout_ref=undefined};
  186
+handler_loop_timeout(State=#state{timeout=Timeout, timeout_ref=PrevRef}) ->
  187
+	_ = case PrevRef of undefined -> ignore; PrevRef ->
  188
+		erlang:cancel_timer(PrevRef) end,
  189
+	TRef = make_ref(),
  190
+	erlang:send_after(Timeout, self(), {?MODULE, timeout, TRef}),
  191
+	State#state{timeout_ref=TRef}.
179 192
 
180 193
 %% @private
181 194
 -spec handler_loop(#state{}, #http_req{}, any(), binary()) -> ok.
182  
-handler_loop(State=#state{messages={OK, Closed, Error}, timeout=Timeout},
  195
+handler_loop(State=#state{messages={OK, Closed, Error}, timeout_ref=TRef},
183 196
 		Req=#http_req{socket=Socket}, HandlerState, SoFar) ->
184 197
 	receive
185 198
 		{OK, Socket, Data} ->
@@ -189,11 +202,13 @@ handler_loop(State=#state{messages={OK, Closed, Error}, timeout=Timeout},
189 202
 			handler_terminate(State, Req, HandlerState, {error, closed});
190 203
 		{Error, Socket, Reason} ->
191 204
 			handler_terminate(State, Req, HandlerState, {error, Reason});
  205
+		{?MODULE, timeout, TRef} ->
  206
+			websocket_close(State, Req, HandlerState, {normal, timeout});
  207
+		{?MODULE, timeout, OlderTRef} when is_reference(OlderTRef) ->
  208
+			handler_loop(State, Req, HandlerState, SoFar);
192 209
 		Message ->
193 210
 			handler_call(State, Req, HandlerState,
194 211
 				SoFar, websocket_info, Message, fun handler_before_loop/4)
195  
-	after Timeout ->
196  
-		websocket_close(State, Req, HandlerState, {normal, timeout})
197 212
 	end.
198 213
 
199 214
 -spec websocket_data(#state{}, #http_req{}, any(), binary()) -> ok.
33  test/http_SUITE.erl
@@ -19,7 +19,8 @@
19 19
 -export([all/0, groups/0, init_per_suite/1, end_per_suite/1,
20 20
 	init_per_group/2, end_per_group/2]). %% ct.
21 21
 -export([chunked_response/1, headers_dupe/1, headers_huge/1,
22  
-	keepalive_nl/1, nc_rand/1, pipeline/1, raw/1, ws0/1, ws8/1]). %% http.
  22
+	keepalive_nl/1, nc_rand/1, pipeline/1, raw/1,
  23
+	ws0/1, ws8/1, ws_timeout_hibernate/1]). %% http.
23 24
 -export([http_200/1, http_404/1]). %% http and https.
24 25
 -export([http_10_hostless/1]). %% misc.
25 26
 
@@ -31,7 +32,8 @@ all() ->
31 32
 groups() ->
32 33
 	BaseTests = [http_200, http_404],
33 34
 	[{http, [], [chunked_response, headers_dupe, headers_huge,
34  
-		keepalive_nl, nc_rand, pipeline, raw, ws0, ws8] ++ BaseTests},
  35
+		keepalive_nl, nc_rand, pipeline, raw,
  36
+		ws0, ws8, ws_timeout_hibernate] ++ BaseTests},
35 37
 	{https, [], BaseTests}, {misc, [], [http_10_hostless]}].
36 38
 
37 39
 init_per_suite(Config) ->
@@ -90,6 +92,7 @@ init_http_dispatch() ->
90 92
 		{[<<"localhost">>], [
91 93
 			{[<<"chunked_response">>], chunked_handler, []},
92 94
 			{[<<"websocket">>], websocket_handler, []},
  95
+			{[<<"ws_timeout_hibernate">>], ws_timeout_hibernate_handler, []},
93 96
 			{[<<"headers">>, <<"dupe">>], http_handler,
94 97
 				[{headers, [{<<"Connection">>, <<"close">>}]}]},
95 98
 			{[], http_handler, []}
@@ -297,6 +300,32 @@ ws8(Config) ->
297 300
 	{error, closed} = gen_tcp:recv(Socket, 0, 6000),
298 301
 	ok.
299 302
 
  303
+ws_timeout_hibernate(Config) ->
  304
+	{port, Port} = lists:keyfind(port, 1, Config),
  305
+	{ok, Socket} = gen_tcp:connect("localhost", Port,
  306
+		[binary, {active, false}, {packet, raw}]),
  307
+	ok = gen_tcp:send(Socket, [
  308
+		"GET /ws_timeout_hibernate HTTP/1.1\r\n"
  309
+		"Host: localhost\r\n"
  310
+		"Connection: Upgrade\r\n"
  311
+		"Upgrade: websocket\r\n"
  312
+		"Sec-WebSocket-Origin: http://localhost\r\n"
  313
+		"Sec-WebSocket-Version: 8\r\n"
  314
+		"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
  315
+		"\r\n"]),
  316
+	{ok, Handshake} = gen_tcp:recv(Socket, 0, 6000),
  317
+	{ok, {http_response, {1, 1}, 101, "Switching Protocols"}, Rest}
  318
+		= erlang:decode_packet(http, Handshake, []),
  319
+	[Headers, <<>>] = websocket_headers(
  320
+		erlang:decode_packet(httph, Rest, []), []),
  321
+	{'Connection', "Upgrade"} = lists:keyfind('Connection', 1, Headers),
  322
+	{'Upgrade', "websocket"} = lists:keyfind('Upgrade', 1, Headers),
  323
+	{"sec-websocket-accept", "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="}
  324
+		= lists:keyfind("sec-websocket-accept", 1, Headers),
  325
+	{ok, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000),
  326
+	{error, closed} = gen_tcp:recv(Socket, 0, 6000),
  327
+	ok.
  328
+
300 329
 websocket_headers({ok, http_eoh, Rest}, Acc) ->
301 330
 	[Acc, Rest];
302 331
 websocket_headers({ok, {http_header, _I, Key, _R, Value}, Rest}, Acc) ->
29  test/ws_timeout_hibernate_handler.erl
... ...
@@ -0,0 +1,29 @@
  1
+%% Feel free to use, reuse and abuse the code in this file.
  2
+
  3
+-module(ws_timeout_hibernate_handler).
  4
+-behaviour(cowboy_http_handler).
  5
+-behaviour(cowboy_http_websocket_handler).
  6
+-export([init/3, handle/2, terminate/2]).
  7
+-export([websocket_init/3, websocket_handle/3,
  8
+	websocket_info/3, websocket_terminate/3]).
  9
+
  10
+init(_Any, _Req, _Opts) ->
  11
+	{upgrade, protocol, cowboy_http_websocket}.
  12
+
  13
+handle(_Req, _State) ->
  14
+	exit(badarg).
  15
+
  16
+terminate(_Req, _State) ->
  17
+	exit(badarg).
  18
+
  19
+websocket_init(_TransportName, Req, _Opts) ->
  20
+	{ok, Req, undefined, 1000, hibernate}.
  21
+
  22
+websocket_handle(_Frame, Req, State) ->
  23
+	{ok, Req, State, hibernate}.
  24
+
  25
+websocket_info(_Info, Req, State) ->
  26
+	{ok, Req, State, hibernate}.
  27
+
  28
+websocket_terminate(_Reason, _Req, _State) ->
  29
+	ok.

0 notes on commit d0f711a

Please sign in to comment.
Something went wrong with that request. Please try again.