diff --git a/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/async/AsyncRandomHandler.java b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/async/AsyncRandomHandler.java index de144e3ad1..27370ee8ef 100644 --- a/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/async/AsyncRandomHandler.java +++ b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/async/AsyncRandomHandler.java @@ -43,6 +43,7 @@ import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.MethodNotSupportedException; +import org.apache.hc.core5.http.NameValuePair; import org.apache.hc.core5.http.ProtocolException; import org.apache.hc.core5.http.message.BasicHttpResponse; import org.apache.hc.core5.http.nio.AsyncEntityProducer; @@ -53,8 +54,11 @@ import org.apache.hc.core5.http.nio.StreamChannel; import org.apache.hc.core5.http.nio.entity.AbstractBinAsyncEntityProducer; import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.net.WWWFormCodec; import org.apache.hc.core5.util.Asserts; +import static java.nio.charset.StandardCharsets.UTF_8; + /** * A handler that generates random data. */ @@ -93,6 +97,22 @@ public void handleRequest( } catch (final URISyntaxException ex) { throw new ProtocolException(ex.getMessage(), ex); } + final String query = uri.getQuery(); + int delayMs = 0; + boolean drip = false; + if (query != null) { + final List params = WWWFormCodec.parse(query, UTF_8); + for (final NameValuePair param : params) { + final String name = param.getName(); + final String value = param.getValue(); + if ("delay".equals(name)) { + delayMs = Integer.parseInt(value); + } else if ("drip".equals(name)) { + drip = "1".equals(value); + } + } + } + final String path = uri.getPath(); final int slash = path.lastIndexOf('/'); if (slash != -1) { @@ -109,7 +129,7 @@ public void handleRequest( n = 1 + (int)(Math.random() * 79.0); } final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK); - final AsyncEntityProducer entityProducer = new RandomBinAsyncEntityProducer(n); + final AsyncEntityProducer entityProducer = new RandomBinAsyncEntityProducer(n, delayMs, drip); entityProducerRef.set(entityProducer); responseChannel.sendResponse(response, entityProducer, context); } else { @@ -162,12 +182,22 @@ public static class RandomBinAsyncEntityProducer extends AbstractBinAsyncEntityP private final long length; private long remaining; private final ByteBuffer buffer; + private final int delayMs; + private final boolean drip; + private volatile long deadline; public RandomBinAsyncEntityProducer(final long len) { + this(len, 0, false); + } + + public RandomBinAsyncEntityProducer(final long len, final int delayMs, final boolean drip) { super(512, ContentType.DEFAULT_TEXT); length = len; remaining = len; buffer = ByteBuffer.allocate(1024); + this.delayMs = delayMs; + this.deadline = System.currentTimeMillis() + (drip ? 0 : delayMs); + this.drip = drip; } @Override @@ -192,6 +222,10 @@ public int availableData() { @Override protected void produceData(final StreamChannel channel) throws IOException { + if (System.currentTimeMillis() < deadline) { + return; + } + final int chunk = Math.min((int) (remaining < Integer.MAX_VALUE ? remaining : Integer.MAX_VALUE), buffer.remaining()); for (int i = 0; i < chunk; i++) { final byte b = RANGE[(int) (Math.random() * RANGE.length)]; @@ -205,6 +239,8 @@ protected void produceData(final StreamChannel channel) throws IOExc if (remaining <= 0 && buffer.position() == 0) { channel.endStream(); + } else if (drip) { + deadline = System.currentTimeMillis() + delayMs; } } diff --git a/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/classic/RandomHandler.java b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/classic/RandomHandler.java index 5c157dd6b7..38715f75f6 100644 --- a/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/classic/RandomHandler.java +++ b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/classic/RandomHandler.java @@ -29,10 +29,12 @@ import java.io.IOException; import java.io.InputStream; +import java.io.InterruptedIOException; import java.io.OutputStream; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; +import java.util.List; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; @@ -40,10 +42,14 @@ import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.MethodNotSupportedException; +import org.apache.hc.core5.http.NameValuePair; import org.apache.hc.core5.http.ProtocolException; import org.apache.hc.core5.http.io.HttpRequestHandler; import org.apache.hc.core5.http.io.entity.AbstractHttpEntity; import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.net.WWWFormCodec; + +import static java.nio.charset.StandardCharsets.UTF_8; /** * A handler that generates random data. @@ -84,6 +90,22 @@ public void handle(final ClassicHttpRequest request, } catch (final URISyntaxException ex) { throw new ProtocolException(ex.getMessage(), ex); } + final String query = uri.getQuery(); + int delayMs = 0; + boolean drip = false; + if (query != null) { + final List params = WWWFormCodec.parse(query, UTF_8); + for (final NameValuePair param : params) { + final String name = param.getName(); + final String value = param.getValue(); + if ("delay".equals(name)) { + delayMs = Integer.parseInt(value); + } else if ("drip".equals(name)) { + drip = "1".equals(value); + } + } + } + final String path = uri.getPath(); final int slash = path.lastIndexOf('/'); if (slash != -1) { @@ -100,7 +122,7 @@ public void handle(final ClassicHttpRequest request, n = 1 + (int)(Math.random() * 79.0); } response.setCode(HttpStatus.SC_OK); - response.setEntity(new RandomEntity(n)); + response.setEntity(new RandomEntity(n, delayMs, drip)); } else { throw new ProtocolException("Invalid request path: " + path); } @@ -120,6 +142,12 @@ public static class RandomEntity extends AbstractHttpEntity { /** The length of the random data to generate. */ protected final long length; + /** The duration of the delay before sending the response entity. */ + protected final int delayMs; + + /** Whether to delay after each chunk sent. If {@code false}, + * will only delay at the start of the response entity. */ + protected final boolean drip; /** * Creates a new entity generating the given amount of data. @@ -128,8 +156,22 @@ public static class RandomEntity extends AbstractHttpEntity { * 0 to maxint */ public RandomEntity(final long len) { + this(len, 0, false); + } + + /** + * Creates a new entity generating the given amount of data. + * + * @param len the number of random bytes to generate, + * 0 to maxint + * @param delayMs how long to wait before sending the first byte + * @param drip whether to repeat the delay after each byte + */ + public RandomEntity(final long len, final int delayMs, final boolean drip) { super((ContentType) null, null); - length = len; + this.length = len; + this.delayMs = delayMs; + this.drip = drip; } /** @@ -185,31 +227,48 @@ public InputStream getContent() { @Override public void writeTo(final OutputStream out) throws IOException { - final int blocksize = 2048; - int remaining = (int) length; // range checked in constructor - final byte[] data = new byte[Math.min(remaining, blocksize)]; + try { + final int blocksize = 2048; + int remaining = (int) length; // range checked in constructor + final byte[] data = new byte[Math.min(remaining, blocksize)]; - while (remaining > 0) { - final int end = Math.min(remaining, data.length); + out.flush(); + if (!drip) { + delay(); + } + while (remaining > 0) { + final int end = Math.min(remaining, data.length); - double value = 0.0; - for (int i = 0; i < end; i++) { - // we get 5 random characters out of one random value - if (i % 5 == 0) { - value = Math.random(); + double value = 0.0; + for (int i = 0; i < end; i++) { + // we get 5 random characters out of one random value + if (i % 5 == 0) { + value = Math.random(); + } + value = value * RANGE.length; + final int d = (int) value; + value = value - d; + data[i] = RANGE[d]; } - value = value * RANGE.length; - final int d = (int) value; - value = value - d; - data[i] = RANGE[d]; - } - out.write(data, 0, end); - out.flush(); + out.write(data, 0, end); + out.flush(); - remaining = remaining - end; + remaining = remaining - end; + if (drip) { + delay(); + } + } + } finally { + out.close(); } - out.close(); + } + private void delay() throws IOException { + try { + Thread.sleep(delayMs); + } catch (final InterruptedException ex) { + throw new InterruptedIOException(); + } } @Override diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractIntegrationTestBase.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractIntegrationTestBase.java index afafb21594..f6affbb046 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractIntegrationTestBase.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractIntegrationTestBase.java @@ -28,6 +28,7 @@ package org.apache.hc.client5.testing.async; import java.net.InetSocketAddress; +import java.nio.file.Path; import java.util.function.Consumer; import org.apache.hc.client5.testing.extension.async.ClientProtocolLevel; @@ -48,9 +49,17 @@ abstract class AbstractIntegrationTestBase { @RegisterExtension private final TestAsyncResources testResources; + private final boolean useUnixDomainSocket; protected AbstractIntegrationTestBase(final URIScheme scheme, final ClientProtocolLevel clientProtocolLevel, final ServerProtocolLevel serverProtocolLevel) { + this(scheme, clientProtocolLevel, serverProtocolLevel, false); + } + + protected AbstractIntegrationTestBase( + final URIScheme scheme, final ClientProtocolLevel clientProtocolLevel, + final ServerProtocolLevel serverProtocolLevel, final boolean useUnixDomainSocket) { this.testResources = new TestAsyncResources(scheme, clientProtocolLevel, serverProtocolLevel, TIMEOUT); + this.useUnixDomainSocket = useUnixDomainSocket; } public URIScheme scheme() { @@ -72,6 +81,9 @@ public void configureServer(final Consumer serverCusto public HttpHost startServer() throws Exception { final TestAsyncServer server = testResources.server(); final InetSocketAddress inetSocketAddress = server.start(); + if (useUnixDomainSocket) { + testResources.udsProxy().start(); + } return new HttpHost(testResources.scheme().id, "localhost", inetSocketAddress.getPort()); } @@ -80,9 +92,21 @@ public void configureClient(final Consumer clientCustomi } public TestAsyncClient startClient() throws Exception { + if (useUnixDomainSocket) { + final Path socketPath = getUnixDomainSocket(); + testResources.configureClient(builder -> { + builder.setUnixDomainSocket(socketPath); + }); + } final TestAsyncClient client = testResources.client(); client.start(); return client; } + public Path getUnixDomainSocket() throws Exception { + if (useUnixDomainSocket) { + return testResources.udsProxy().getSocketPath(); + } + return null; + } } diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestAsyncSocketTimeout.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestAsyncSocketTimeout.java new file mode 100644 index 0000000000..4911cbec5b --- /dev/null +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/TestAsyncSocketTimeout.java @@ -0,0 +1,166 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.client5.testing.async; + +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; +import org.apache.hc.client5.http.config.ConnectionConfig; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager; +import org.apache.hc.client5.testing.extension.async.ClientProtocolLevel; +import org.apache.hc.client5.testing.extension.async.ServerProtocolLevel; +import org.apache.hc.client5.testing.extension.async.TestAsyncClient; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.Method; +import org.apache.hc.core5.http.URIScheme; +import org.apache.hc.core5.io.CloseMode; +import org.apache.hc.core5.util.VersionInfo; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.net.SocketTimeoutException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import static java.lang.String.format; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.apache.hc.core5.util.ReflectionUtils.determineJRELevel; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeFalse; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +abstract class AbstractTestSocketTimeout extends AbstractIntegrationTestBase { + protected AbstractTestSocketTimeout(final URIScheme scheme, final ClientProtocolLevel clientProtocolLevel, + final ServerProtocolLevel serverProtocolLevel, final boolean useUnixDomainSocket) { + super(scheme, clientProtocolLevel, serverProtocolLevel, useUnixDomainSocket); + } + + @Timeout(5) + @ParameterizedTest + @ValueSource(strings = { + "150,0,150,false", + "0,150,150,false", + "150,0,150,true", + "0,150,150,true", + // ResponseTimeout overrides socket timeout + "2000,150,150,false", + "2000,150,150,true" + }) + void testReadTimeouts(final String param) throws Throwable { + checkAssumptions(); + final String[] params = param.split(","); + final int connConfigTimeout = Integer.parseInt(params[0]); + final long responseTimeout = Integer.parseInt(params[1]); + final long expectedDelayMs = Long.parseLong(params[2]); + final boolean drip = Boolean.parseBoolean(params[3]); + configureServer(bootstrap -> bootstrap + .register("/random/*", AsyncRandomHandler::new)); + final HttpHost target = startServer(); + + final TestAsyncClient client = startClient(); + final PoolingAsyncClientConnectionManager connManager = client.getConnectionManager(); + if (connConfigTimeout > 0) { + connManager.setDefaultConnectionConfig(ConnectionConfig.custom() + .setSocketTimeout(connConfigTimeout, MILLISECONDS) + .build()); + } + final SimpleHttpRequest request = SimpleHttpRequest.create(Method.GET, target, + "/random/10240?delay=500&drip=" + (drip ? 1 : 0)); + if (responseTimeout > 0) { + request.setConfig(RequestConfig.custom() + .setUnixDomainSocket(getUnixDomainSocket()) + .setResponseTimeout(responseTimeout, MILLISECONDS).build()); + } + + final long startTime = System.nanoTime(); + final Throwable cause = assertThrows(ExecutionException.class, () -> client.execute(request, null).get()) + .getCause(); + final long actualDelayMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime); + + assertInstanceOf(SocketTimeoutException.class, cause); + assertTrue(actualDelayMs > expectedDelayMs / 2, + format("Socket read timed out too soon (only %,d out of %,d ms)", actualDelayMs, expectedDelayMs)); + assertTrue(actualDelayMs < expectedDelayMs * 2, + format("Socket read timed out too late (%,d out of %,d ms)", actualDelayMs, expectedDelayMs)); + + closeClient(client); + } + + void checkAssumptions() { + } + + void closeClient(final TestAsyncClient client) { + client.close(CloseMode.GRACEFUL); + } +} + +public class TestAsyncSocketTimeout { + @Nested + class Http extends AbstractTestSocketTimeout { + public Http() { + super(URIScheme.HTTP, ClientProtocolLevel.STANDARD, ServerProtocolLevel.STANDARD, false); + } + } + + @Nested + class Https extends AbstractTestSocketTimeout { + public Https() { + super(URIScheme.HTTPS, ClientProtocolLevel.STANDARD, ServerProtocolLevel.STANDARD, false); + } + } + + @Nested + class Uds extends AbstractTestSocketTimeout { + public Uds() { + super(URIScheme.HTTP, ClientProtocolLevel.STANDARD, ServerProtocolLevel.STANDARD, true); + } + + @Override + void checkAssumptions() { + assumeTrue(determineJRELevel() >= 16, "Async UDS requires Java 16+"); + final String[] components = VersionInfo + .loadVersionInfo("org.apache.hc.core5", getClass().getClassLoader()) + .getRelease() + .split("[-.]"); + final int majorVersion = Integer.parseInt(components[0]); + final int minorVersion = Integer.parseInt(components[1]); + assumeFalse(majorVersion <= 5 && minorVersion <= 3, "Async UDS requires HttpCore 5.4+"); + } + + @Override + void closeClient(final TestAsyncClient client) { + // TODO: Why does `CloseMode.GRACEFUL` (the test client default) + // block until the IOReactor shutdown timeout when using UDS? + client.close(CloseMode.IMMEDIATE); + } + } +} + diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/StandardTestClientBuilder.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/StandardTestClientBuilder.java index 1df164a8c6..efaa6d1081 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/StandardTestClientBuilder.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/StandardTestClientBuilder.java @@ -27,6 +27,7 @@ package org.apache.hc.client5.testing.extension.async; +import java.nio.file.Path; import java.util.Collection; import org.apache.hc.client5.http.AuthenticationStrategy; @@ -35,6 +36,7 @@ import org.apache.hc.client5.http.auth.AuthSchemeFactory; import org.apache.hc.client5.http.auth.CredentialsProvider; import org.apache.hc.client5.http.config.ConnectionConfig; +import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.config.TlsConfig; import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder; @@ -50,6 +52,7 @@ import org.apache.hc.core5.http.nio.ssl.TlsStrategy; import org.apache.hc.core5.http2.config.H2Config; import org.apache.hc.core5.reactor.IOReactorConfig; +import org.apache.hc.core5.util.TimeValue; import org.apache.hc.core5.util.Timeout; final class StandardTestClientBuilder implements TestAsyncClientBuilder { @@ -166,6 +169,14 @@ public TestAsyncClientBuilder setDefaultCredentialsProvider(final CredentialsPro return this; } + @Override + public TestAsyncClientBuilder setUnixDomainSocket(final Path unixDomainSocket) { + this.clientBuilder.setDefaultRequestConfig(RequestConfig.custom() + .setUnixDomainSocket(unixDomainSocket) + .build()); + return this; + } + @Override public TestAsyncClient build() throws Exception { final PoolingAsyncClientConnectionManager connectionManager = connectionManagerBuilder @@ -177,6 +188,7 @@ public TestAsyncClient build() throws Exception { .build(); final CloseableHttpAsyncClient client = clientBuilder .setIOReactorConfig(IOReactorConfig.custom() + .setSelectInterval(TimeValue.ofMilliseconds(10)) .setSoTimeout(timeout) .build()) .setConnectionManager(connectionManager) diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/TestAsyncClientBuilder.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/TestAsyncClientBuilder.java index 3f12b77cf0..4107a4cc43 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/TestAsyncClientBuilder.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/TestAsyncClientBuilder.java @@ -27,6 +27,7 @@ package org.apache.hc.client5.testing.extension.async; +import java.nio.file.Path; import java.util.Collection; import org.apache.hc.client5.http.AuthenticationStrategy; @@ -110,6 +111,10 @@ default TestAsyncClientBuilder setDefaultCredentialsProvider(CredentialsProvider throw new UnsupportedOperationException("Operation not supported by " + getProtocolLevel()); } + default TestAsyncClientBuilder setUnixDomainSocket(Path unixDomainSocket) { + throw new UnsupportedOperationException("Operation not supported by " + getProtocolLevel()); + } + TestAsyncClient build() throws Exception; } diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/TestAsyncResources.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/TestAsyncResources.java index 0c470fbdf9..6cb512c347 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/TestAsyncResources.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/TestAsyncResources.java @@ -30,6 +30,7 @@ import java.util.function.Consumer; import org.apache.hc.client5.testing.extension.sync.TestClientResources; +import org.apache.hc.client5.testing.extension.sync.UnixDomainProxyServer; import org.apache.hc.core5.http.URIScheme; import org.apache.hc.core5.io.CloseMode; import org.apache.hc.core5.util.Asserts; @@ -52,6 +53,7 @@ public class TestAsyncResources implements AfterEachCallback { private TestAsyncServer server; private TestAsyncClient client; + private UnixDomainProxyServer udsProxy; public TestAsyncResources(final URIScheme scheme, final ClientProtocolLevel clientProtocolLevel, final ServerProtocolLevel serverProtocolLevel, final Timeout timeout) { this.scheme = scheme != null ? scheme : URIScheme.HTTP; @@ -84,11 +86,23 @@ public void afterEach(final ExtensionContext extensionContext) { if (client != null) { client.close(CloseMode.GRACEFUL); } + if (udsProxy != null) { + udsProxy.close(); + } if (server != null) { server.shutdown(TimeValue.ofSeconds(5)); } } + public UnixDomainProxyServer udsProxy() throws Exception { + if (udsProxy == null) { + final TestAsyncServer testServer = server(); + final int port = testServer.getServerAddress().getPort(); + udsProxy = new UnixDomainProxyServer(port); + } + return udsProxy; + } + public URIScheme scheme() { return this.scheme; } diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/TestAsyncServer.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/TestAsyncServer.java index e75ef11b6e..96e3e4d381 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/TestAsyncServer.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/TestAsyncServer.java @@ -48,6 +48,7 @@ public class TestAsyncServer { private final Http1Config http1Config; private final HttpProcessor httpProcessor; private final Decorator exchangeHandlerDecorator; + private volatile InetSocketAddress serverAddress; TestAsyncServer( final H2TestServer server, @@ -98,7 +99,14 @@ public InetSocketAddress start() throws Exception { } server.configure(exchangeHandlerDecorator); server.configure(httpProcessor); - return server.start(); + serverAddress = server.start(); + return serverAddress; } + public InetSocketAddress getServerAddress() { + if (serverAddress == null) { + throw new IllegalStateException("Server has not been started"); + } + return serverAddress; + } } diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/AbstractIntegrationTestBase.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/AbstractIntegrationTestBase.java index 6e6b24b893..3071a38da0 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/AbstractIntegrationTestBase.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/AbstractIntegrationTestBase.java @@ -87,7 +87,7 @@ public void configureClient(final Consumer clientCustomizer) public TestClient client() throws Exception { if (useUnixDomainSocket) { - final Path socketPath = testResources.udsProxy().getSocketPath(); + final Path socketPath = getUnixDomainSocket(); testResources.configureClient(builder -> { builder.setUnixDomainSocket(socketPath); }); @@ -95,4 +95,10 @@ public TestClient client() throws Exception { return testResources.client(); } + public Path getUnixDomainSocket() throws Exception { + if (useUnixDomainSocket) { + return testResources.udsProxy().getSocketPath(); + } + return null; + } } diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestSocketTimeout.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestSocketTimeout.java new file mode 100644 index 0000000000..2c8194231f --- /dev/null +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestSocketTimeout.java @@ -0,0 +1,142 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.client5.testing.sync; + +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.config.ConnectionConfig; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.classic.BasicHttpClientResponseHandler; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; +import org.apache.hc.client5.testing.classic.RandomHandler; +import org.apache.hc.client5.testing.extension.sync.ClientProtocolLevel; +import org.apache.hc.client5.testing.extension.sync.TestClient; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.URIScheme; +import org.apache.hc.core5.http.io.SocketConfig; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.net.SocketTimeoutException; +import java.net.URI; +import java.util.concurrent.TimeUnit; + +import static java.lang.String.format; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +abstract class AbstractTestSocketTimeout extends AbstractIntegrationTestBase { + protected AbstractTestSocketTimeout(final URIScheme scheme, final ClientProtocolLevel clientProtocolLevel, + final boolean useUnixDomainSocket) { + super(scheme, clientProtocolLevel, useUnixDomainSocket); + } + + @Timeout(5) + @ParameterizedTest + @ValueSource(strings = { + "150,0,0,150,false", + "0,150,0,150,false", + "150,0,0,150,true", + "0,150,0,150,true", + // ConnectionConfig overrides SocketConfig + "50,150,0,150,false", + "1000,150,0,150,false", + "50,150,0,150,true", + "1000,150,0,150,true", + // ResponseTimeout overrides socket timeout + "2000,2000,150,150,false", + "2000,2000,150,150,true" + }) + void testReadTimeouts(final String param) throws Exception { + final String[] params = param.split(","); + final int socketConfigTimeout = Integer.parseInt(params[0]); + final int connConfigTimeout = Integer.parseInt(params[1]); + final long responseTimeout = Integer.parseInt(params[2]); + final long expectedDelayMs = Long.parseLong(params[3]); + final boolean drip = Boolean.parseBoolean(params[4]); + configureServer(bootstrap -> bootstrap + .register("/random/*", new RandomHandler())); + final HttpHost target = startServer(); + + final TestClient client = client(); + final PoolingHttpClientConnectionManager connManager = client.getConnectionManager(); + if (socketConfigTimeout > 0) { + connManager.setDefaultSocketConfig(SocketConfig.custom() + .setSoTimeout(socketConfigTimeout, MILLISECONDS) + .build()); + } + if (connConfigTimeout > 0) { + connManager.setDefaultConnectionConfig(ConnectionConfig.custom() + .setSocketTimeout(connConfigTimeout, MILLISECONDS) + .build()); + } + final HttpGet request = new HttpGet(new URI("/random/10240?delay=1000&drip=" + (drip ? 1 : 0))); + if (responseTimeout > 0) { + request.setConfig(RequestConfig.custom() + .setUnixDomainSocket(getUnixDomainSocket()) + .setResponseTimeout(responseTimeout, MILLISECONDS) + .build()); + } + + final long startTime = System.nanoTime(); + assertThrows(SocketTimeoutException.class, () -> + client.execute(target, request, new BasicHttpClientResponseHandler())); + final long actualDelayMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime); + + assertTrue(actualDelayMs > expectedDelayMs / 2, + format("Socket read timed out too soon (only %,d out of %,d ms)", actualDelayMs, expectedDelayMs)); + assertTrue(actualDelayMs < expectedDelayMs * 2, + format("Socket read timed out too late (%,d out of %,d ms)", actualDelayMs, expectedDelayMs)); + } +} + +public class TestSocketTimeout { + @Nested + class Http extends AbstractTestSocketTimeout { + public Http() { + super(URIScheme.HTTP, ClientProtocolLevel.STANDARD, false); + } + } + + @Nested + class Https extends AbstractTestSocketTimeout { + public Https() { + super(URIScheme.HTTP, ClientProtocolLevel.STANDARD, false); + } + } + + @Nested + class Uds extends AbstractTestSocketTimeout { + public Uds() { + super(URIScheme.HTTP, ClientProtocolLevel.STANDARD, true); + } + } +} +