Browse Source

Add an option to disable sendfile for a listener

Loïc Hoguin 6 years ago
parent
commit
be09711687

+ 7 - 0
doc/src/manual/cowboy_http.asciidoc

@@ -32,6 +32,7 @@ opts() :: #{
     max_skip_body_length    => non_neg_integer(),
     middlewares             => [module()],
     request_timeout         => timeout(),
+    sendfile                => boolean(),
     shutdown_timeout        => timeout(),
     stream_handlers         => [module()]
 }
@@ -96,6 +97,11 @@ middlewares ([cowboy_router, cowboy_handler])::
 request_timeout (5000)::
     Time in ms with no requests before Cowboy closes the connection.
 
+sendfile (true)::
+    Whether the sendfile syscall may be used. It can be useful to disable
+    it on systems where the syscall has a buggy implementation, for example
+    under VirtualBox when using shared folders.
+
 shutdown_timeout (5000)::
     Time in ms Cowboy will wait for child processes to shut down before killing them.
 
@@ -104,6 +110,7 @@ stream_handlers ([cowboy_stream_h])::
 
 == Changelog
 
+* *2.6*: The `sendfile` option was added.
 * *2.5*: The `linger_timeout` option was added.
 * *2.2*: The `max_skip_body_length` option was added.
 * *2.0*: The `timeout` option was renamed `request_timeout`.

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

@@ -30,6 +30,7 @@ opts() :: #{
     max_frame_size_sent            => 16384..16777215 | infinity,
     middlewares                    => [module()],
     preface_timeout                => timeout(),
+    sendfile                       => boolean(),
     settings_timeout               => timeout(),
     shutdown_timeout               => timeout(),
     stream_handlers                => [module()]
@@ -119,6 +120,12 @@ preface_timeout (5000)::
 
 Time in ms Cowboy is willing to wait for the connection preface.
 
+sendfile (true)::
+
+Whether the sendfile syscall may be used. It can be useful to disable
+it on systems where the syscall has a buggy implementation, for example
+under VirtualBox when using shared folders.
+
 settings_timeout (5000)::
 
 Time in ms Cowboy is willing to wait for a SETTINGS ack.
@@ -133,6 +140,7 @@ Ordered list of stream handlers that will handle all stream events.
 
 == Changelog
 
+* *2.6*: The `sendfile` option was added.
 * *2.4*: Add the options `initial_connection_window_size`,
          `initial_stream_window_size`, `max_concurrent_streams`,
          `max_decode_table_size`, `max_encode_table_size`,

+ 7 - 2
src/cowboy_http.erl

@@ -44,6 +44,7 @@
 	middlewares => [module()],
 	proxy_header => boolean(),
 	request_timeout => timeout(),
+	sendfile => boolean(),
 	shutdown_timeout => timeout(),
 	stream_handlers => [module()],
 	tracer_callback => cowboy_tracer_h:tracer_callback(),
@@ -1050,7 +1051,7 @@ commands(State=#state{socket=Socket, transport=Transport, streams=Streams, out_s
 	end,
 	commands(State#state{out_state=done}, StreamID, Tail);
 %% Send a file.
-commands(State0=#state{socket=Socket, transport=Transport}, StreamID,
+commands(State0=#state{socket=Socket, transport=Transport, opts=Opts}, StreamID,
 		[{sendfile, IsFin, Offset, Bytes, Path}|Tail]) ->
 	%% @todo exit with response_body_too_large if we exceed content-length
 	%% We wrap the sendfile call into a try/catch because on OTP-20
@@ -1066,7 +1067,11 @@ commands(State0=#state{socket=Socket, transport=Transport}, StreamID,
 	%% This try/catch prevents some noisy logs to be written
 	%% when these errors occur.
 	try
-		Transport:sendfile(Socket, Path, Offset, Bytes),
+		%% When sendfile is disabled we explicitly use the fallback.
+		_ = case maps:get(sendfile, Opts, true) of
+			true -> Transport:sendfile(Socket, Path, Offset, Bytes);
+			false -> ranch_transport:sendfile(Transport, Socket, Path, Offset, Bytes, [])
+		end,
 		State = case IsFin of
 			fin -> State0#state{out_state=done}
 %% @todo Add the sendfile command.

+ 7 - 2
src/cowboy_http2.erl

@@ -44,6 +44,7 @@
 	middlewares => [module()],
 	preface_timeout => timeout(),
 	proxy_header => boolean(),
+	sendfile => boolean(),
 	settings_timeout => timeout(),
 	shutdown_timeout => timeout(),
 	stream_handlers => [module()],
@@ -650,10 +651,14 @@ send_data_frame(State=#state{socket=Socket, transport=Transport},
 		StreamID, IsFin, {data, Data}) ->
 	Transport:send(Socket, cow_http2:data(StreamID, IsFin, Data)),
 	State;
-send_data_frame(State=#state{socket=Socket, transport=Transport},
+send_data_frame(State=#state{socket=Socket, transport=Transport, opts=Opts},
 		StreamID, IsFin, {sendfile, Offset, Bytes, Path}) ->
 	Transport:send(Socket, cow_http2:data_header(StreamID, IsFin, Bytes)),
-	Transport:sendfile(Socket, Path, Offset, Bytes),
+	%% When sendfile is disabled we explicitly use the fallback.
+	_ = case maps:get(sendfile, Opts, true) of
+		true -> Transport:sendfile(Socket, Path, Offset, Bytes);
+		false -> ranch_transport:sendfile(Transport, Socket, Path, Offset, Bytes, [])
+	end,
 	State;
 %% The stream is terminated in cow_http2_machine:prepare_trailers.
 send_data_frame(State=#state{socket=Socket, transport=Transport,

+ 19 - 4
test/static_handler_SUITE.erl

@@ -23,7 +23,10 @@
 %% ct.
 
 all() ->
-	cowboy_test:common_all().
+	cowboy_test:common_all() ++ [
+		{group, http_no_sendfile},
+		{group, h2c_no_sendfile}
+	].
 
 groups() ->
 	AllTests = ct_helper:all(?MODULE),
@@ -44,7 +47,10 @@ groups() ->
 		{http_compress, [parallel], GroupTests},
 		{https_compress, [parallel], GroupTests},
 		{h2_compress, [parallel], GroupTests},
-		{h2c_compress, [parallel], GroupTests}
+		{h2c_compress, [parallel], GroupTests},
+		%% No real need to test sendfile disabled against https or h2.
+		{http_no_sendfile, [parallel], GroupTests},
+		{h2c_no_sendfile, [parallel], GroupTests}
 	].
 
 init_per_suite(Config) ->
@@ -94,8 +100,17 @@ init_per_group(dir, Config) ->
 	[{prefix, "/dir"}|Config];
 init_per_group(priv_dir, Config) ->
 	[{prefix, "/priv_dir"}|Config];
-init_per_group(tttt, Config) ->
-	Config;
+init_per_group(Name=http_no_sendfile, Config) ->
+	cowboy_test:init_http(Name, #{
+		env => #{dispatch => init_dispatch(Config)},
+		sendfile => false
+	}, [{flavor, vanilla}|Config]);
+init_per_group(Name=h2c_no_sendfile, Config) ->
+	Config1 = cowboy_test:init_http(Name, #{
+		env => #{dispatch => init_dispatch(Config)},
+		sendfile => false
+	}, [{flavor, vanilla}|Config]),
+	lists:keyreplace(protocol, 1, Config1, {protocol, http2});
 init_per_group(Name, Config) ->
 	cowboy_test:init_common_groups(Name, Config, ?MODULE).