Skip to content

Commit 420d4cc

Browse files
authored
Send chunked file fixes
- Extract HTTP send file tests in their own hiearchy - Avoid closing chunked file writes when using directly descriptors - Avoid premature connection close when sending file with TLS with a configured idle timeout
2 parents f1b9d4f + 2b2bc8f commit 420d4cc

9 files changed

Lines changed: 595 additions & 460 deletions

File tree

vertx-core/src/main/java/io/vertx/core/net/impl/NetServerImpl.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -266,16 +266,16 @@ protected void initChannel(ChannelPipeline pipeline, boolean ssl) {
266266
if (options.getLogActivity()) {
267267
pipeline.addLast("logging", new LoggingHandler(options.getActivityLogDataFormat()));
268268
}
269-
if (ssl || !options.isFileRegionEnabled() || !vertx.transport().supportFileRegion() || (options.getTrafficShapingOptions() != null && options.getTrafficShapingOptions().getOutboundGlobalBandwidth() > 0)) {
270-
// only add ChunkedWriteHandler when SSL is enabled or FileRegion isn't supported or when outbound traffic shaping is enabled
271-
pipeline.addLast("chunkedWriter", new ChunkedWriteHandler()); // For large file / sendfile support
272-
}
273269
int idleTimeout = options.getIdleTimeout();
274270
int readIdleTimeout = options.getReadIdleTimeout();
275271
int writeIdleTimeout = options.getWriteIdleTimeout();
276272
if (idleTimeout > 0 || readIdleTimeout > 0 || writeIdleTimeout > 0) {
277273
pipeline.addLast("idle", new IdleStateHandler(readIdleTimeout, writeIdleTimeout, idleTimeout, options.getIdleTimeoutUnit()));
278274
}
275+
if (ssl || !options.isFileRegionEnabled() || !vertx.transport().supportFileRegion() || (options.getTrafficShapingOptions() != null && options.getTrafficShapingOptions().getOutboundGlobalBandwidth() > 0)) {
276+
// only add ChunkedWriteHandler when SSL is enabled or FileRegion isn't supported or when outbound traffic shaping is enabled
277+
pipeline.addLast("chunkedWriter", new ChunkedWriteHandler()); // For large file / sendfile support
278+
}
279279
}
280280

281281
protected GlobalTrafficShapingHandler createTrafficShapingHandler() {

vertx-core/src/main/java/io/vertx/core/net/impl/VertxConnection.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,12 @@ public ChannelFuture sendFile(FileChannel fc, long offset, long length) {
499499
if (!supportsFileRegion()) {
500500
// Cannot use zero-copy
501501
try {
502-
writeToChannel(new ChunkedNioFile(fc, offset, length, 8192), writeFuture);
502+
writeToChannel(new ChunkedNioFile(fc, offset, length, 8192) {
503+
@Override
504+
public void close() {
505+
// The called is responsible for closing the file
506+
}
507+
}, writeFuture);
503508
} catch (IOException e) {
504509
return chctx.newFailedFuture(e);
505510
}

vertx-core/src/test/java/io/vertx/test/http/HttpTestBase.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,13 @@
2323
import io.vertx.test.proxy.HttpProxy;
2424
import io.vertx.test.proxy.SocksProxy;
2525
import io.vertx.test.proxy.TestProxyBase;
26+
import org.junit.Rule;
27+
import org.junit.rules.TemporaryFolder;
2628

29+
import java.io.BufferedWriter;
2730
import java.io.File;
31+
import java.io.FileOutputStream;
32+
import java.io.OutputStreamWriter;
2833
import java.util.concurrent.CompletableFuture;
2934
import java.util.concurrent.ExecutionException;
3035
import java.util.concurrent.TimeUnit;
@@ -43,12 +48,16 @@ public class HttpTestBase extends VertxTestBase {
4348
public static final String DEFAULT_HTTPS_HOST_AND_PORT = DEFAULT_HTTPS_HOST + ":" + DEFAULT_HTTPS_PORT;;
4449
public static final String DEFAULT_TEST_URI = "some-uri";
4550

51+
@Rule
52+
public final TemporaryFolder testFolder = TemporaryFolder.builder().assureDeletion().build();
53+
4654
protected HttpServer server;
4755
protected HttpClientAgent client;
4856
protected TestProxyBase proxy;
4957
protected SocketAddress testAddress;
5058
protected RequestOptions requestOptions;
5159
private File tmp;
60+
private File testDir;
5261

5362
protected HttpServerOptions createBaseServerOptions() {
5463
return new HttpServerOptions().setPort(DEFAULT_HTTP_PORT).setHost(DEFAULT_HTTP_HOST);
@@ -70,6 +79,12 @@ public void setUp() throws Exception {
7079
client = vertx.createHttpClient(createBaseClientOptions());
7180
}
7281

82+
@Override
83+
public void after() throws Exception {
84+
testDir = null;
85+
super.after();
86+
}
87+
7388
/**
7489
* Override to disable domain sockets testing.
7590
*/
@@ -163,4 +178,22 @@ protected void startProxy(String username, ProxyType proxyType) throws Exception
163178
proxy.username(username);
164179
proxy.start(vertx);
165180
}
181+
182+
protected File testDir() throws Exception {
183+
if (testDir == null) {
184+
testDir = testFolder.newFolder();
185+
}
186+
return testDir;
187+
}
188+
189+
protected File setupFile(String fileName, String content) throws Exception {
190+
File file = new File(testDir(), fileName);
191+
if (file.exists()) {
192+
file.delete();
193+
}
194+
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8"));
195+
out.write(content);
196+
out.close();
197+
return file;
198+
}
166199
}

vertx-core/src/test/java/io/vertx/tests/http/Http1xTest.java

Lines changed: 0 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import io.vertx.core.http.impl.headers.HeadersMultiMap;
2525
import io.vertx.core.impl.SysProps;
2626
import io.vertx.core.internal.ContextInternal;
27-
import io.vertx.core.impl.Utils;
2827
import io.vertx.core.internal.VertxInternal;
2928
import io.vertx.core.internal.http.HttpServerRequestInternal;
3029
import io.vertx.core.json.JsonArray;
@@ -41,7 +40,6 @@
4140
import org.junit.Ignore;
4241
import org.junit.Test;
4342

44-
import java.io.File;
4543
import java.util.*;
4644
import java.util.concurrent.*;
4745
import java.util.concurrent.atomic.AtomicBoolean;
@@ -3787,29 +3785,6 @@ private void testPerXXXPooling(Function<Integer, Future<HttpClientRequest>> requ
37873785
await();
37883786
}
37893787

3790-
@Test
3791-
public void testSendFileFailsWhenClientClosesConnection() throws Exception {
3792-
// 10 megs
3793-
final File f = setupFile("file.pdf", TestUtils.randomUnicodeString(10 * 1024 * 1024));
3794-
server.requestHandler(req -> {
3795-
try {
3796-
req.response().sendFile(f.getAbsolutePath()).onComplete(onFailure(err -> {
3797-
// Broken pipe
3798-
testComplete();
3799-
}));
3800-
} catch (Exception e) {
3801-
// this was the bug reported with issues/issue-80
3802-
fail(e);
3803-
}
3804-
});
3805-
startServer(testAddress);
3806-
vertx.createNetClient().connect(testAddress).onComplete(onSuccess(socket -> {
3807-
socket.write("GET / HTTP/1.1\r\n\r\n");
3808-
socket.close();
3809-
}));
3810-
await();
3811-
}
3812-
38133788
// Use a raw socket to check the body response is effectively empty (it could be an empty chunk)
38143789
protected MultiMap checkEmptyHttpResponse(HttpMethod method, int sc, MultiMap reqHeaders) throws Exception {
38153790
server.requestHandler(req -> {
@@ -4870,82 +4845,6 @@ public void start(Promise<Void> startFuture) {
48704845
await();
48714846
}
48724847

4873-
@Test
4874-
public void testHttpServerWithIdleTimeoutSendChunkedFile() throws Exception {
4875-
// Does not pass reliably in CI (timeout)
4876-
Assume.assumeTrue(!vertx.isNativeTransportEnabled() && !Utils.isWindows());
4877-
int expected = 32 * 1024 * 1024;
4878-
File file = TestUtils.tmpFile(".dat", expected);
4879-
// Estimate the delay to transfer a file with a 1ms pause in chunks
4880-
int delay = retrieveFileFromServer(file, createBaseServerOptions());
4881-
// Now test with timeout relative to this delay
4882-
int timeout = delay / 2;
4883-
delay = retrieveFileFromServer(file, createBaseServerOptions().setIdleTimeout(timeout).setIdleTimeoutUnit(TimeUnit.MILLISECONDS));
4884-
assertTrue(delay > timeout);
4885-
}
4886-
4887-
private int retrieveFileFromServer(File file, HttpServerOptions options) throws Exception {
4888-
server.close().await();
4889-
server = vertx
4890-
.createHttpServer(options)
4891-
.requestHandler(
4892-
req -> {
4893-
req.response().sendFile(file.getAbsolutePath());
4894-
});
4895-
startServer(testAddress);
4896-
long now = System.currentTimeMillis();
4897-
Integer len = getFile().await();
4898-
assertEquals((int)len, file.length());
4899-
return (int) (System.currentTimeMillis() - now);
4900-
}
4901-
4902-
private Future<Integer> getFile() {
4903-
int[] length = {0};
4904-
return client.request(requestOptions)
4905-
.compose(req -> req.send()
4906-
.compose(resp -> {
4907-
resp.handler(buff -> {
4908-
length[0] += buff.length();
4909-
resp.pause();
4910-
vertx.setTimer(1, id -> {
4911-
resp.resume();
4912-
});
4913-
});
4914-
resp.exceptionHandler(this::fail);
4915-
return resp.end();
4916-
}))
4917-
.map(v -> length[0]);
4918-
}
4919-
4920-
@Test
4921-
public void testSendFilePipelined() throws Exception {
4922-
int n = 2;
4923-
waitFor(n);
4924-
File sent = TestUtils.tmpFile(".dat", 16 * 1024);
4925-
server.requestHandler(
4926-
req -> {
4927-
req.response().sendFile(sent.getAbsolutePath());
4928-
});
4929-
startServer(testAddress);
4930-
client.close();
4931-
client = vertx.createHttpClient(createBaseClientOptions().setPipelining(true), new PoolOptions().setHttp1MaxSize(1));
4932-
for (int i = 0;i < n;i++) {
4933-
client.request(requestOptions)
4934-
.compose(req -> req.send().compose(HttpClientResponse::body))
4935-
.onComplete(onSuccess(body -> {
4936-
complete();
4937-
}));
4938-
}
4939-
await();
4940-
}
4941-
4942-
@Test
4943-
public void testSendFileWithConnectionCloseHeader() throws Exception {
4944-
String content = TestUtils.randomUnicodeString(1024 * 1024 * 2);
4945-
sendFile("test-send-file.html", content, false,
4946-
() -> client.request(requestOptions).map(req -> req.putHeader(HttpHeaders.CONNECTION, "close")));
4947-
}
4948-
49494848
@Test
49504849
public void testResponseEndHandlersConnectionClose() throws Exception {
49514850
waitFor(2);

0 commit comments

Comments
 (0)