| // |
| // ======================================================================== |
| // Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd. |
| // ------------------------------------------------------------------------ |
| // All rights reserved. This program and the accompanying materials |
| // are made available under the terms of the Eclipse Public License v1.0 |
| // and Apache License v2.0 which accompanies this distribution. |
| // |
| // The Eclipse Public License is available at |
| // http://www.eclipse.org/legal/epl-v10.html |
| // |
| // The Apache License v2.0 is available at |
| // http://www.opensource.org/licenses/apache2.0.php |
| // |
| // You may elect to redistribute this code under either of these licenses. |
| // ======================================================================== |
| // |
| |
| package org.eclipse.jetty.client; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.IOException; |
| import java.net.InetSocketAddress; |
| import java.net.Socket; |
| import java.net.SocketTimeoutException; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.Executor; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.TimeoutException; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import java.util.concurrent.atomic.AtomicInteger; |
| |
| import javax.net.ssl.SSLEngine; |
| import javax.servlet.ServletException; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| |
| import org.eclipse.jetty.client.api.Connection; |
| import org.eclipse.jetty.client.api.Destination; |
| import org.eclipse.jetty.client.api.Request; |
| import org.eclipse.jetty.client.api.Response; |
| import org.eclipse.jetty.client.api.Result; |
| import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; |
| import org.eclipse.jetty.client.http.HttpDestinationOverHTTP; |
| import org.eclipse.jetty.client.util.BufferingResponseListener; |
| import org.eclipse.jetty.client.util.InputStreamContentProvider; |
| import org.eclipse.jetty.io.ByteBufferPool; |
| import org.eclipse.jetty.io.ClientConnectionFactory; |
| import org.eclipse.jetty.io.EndPoint; |
| import org.eclipse.jetty.io.ssl.SslClientConnectionFactory; |
| import org.eclipse.jetty.io.ssl.SslConnection; |
| import org.eclipse.jetty.server.handler.AbstractHandler; |
| import org.eclipse.jetty.toolchain.test.annotation.Slow; |
| import org.eclipse.jetty.util.FuturePromise; |
| import org.eclipse.jetty.util.IO; |
| import org.eclipse.jetty.util.ssl.SslContextFactory; |
| import org.hamcrest.Matchers; |
| import org.junit.Assert; |
| import org.junit.Assume; |
| import org.junit.Test; |
| |
| public class HttpClientTimeoutTest extends AbstractHttpClientServerTest |
| { |
| public HttpClientTimeoutTest(SslContextFactory sslContextFactory) |
| { |
| super(sslContextFactory); |
| } |
| |
| @Slow |
| @Test(expected = TimeoutException.class) |
| public void testTimeoutOnFuture() throws Exception |
| { |
| long timeout = 1000; |
| start(new TimeoutHandler(2 * timeout)); |
| |
| client.newRequest("localhost", connector.getLocalPort()) |
| .scheme(scheme) |
| .timeout(timeout, TimeUnit.MILLISECONDS) |
| .send(); |
| } |
| |
| @Slow |
| @Test |
| public void testTimeoutOnListener() throws Exception |
| { |
| long timeout = 1000; |
| start(new TimeoutHandler(2 * timeout)); |
| |
| final CountDownLatch latch = new CountDownLatch(1); |
| Request request = client.newRequest("localhost", connector.getLocalPort()) |
| .scheme(scheme) |
| .timeout(timeout, TimeUnit.MILLISECONDS); |
| request.send(new Response.CompleteListener() |
| { |
| @Override |
| public void onComplete(Result result) |
| { |
| Assert.assertTrue(result.isFailed()); |
| latch.countDown(); |
| } |
| }); |
| Assert.assertTrue(latch.await(3 * timeout, TimeUnit.MILLISECONDS)); |
| } |
| |
| @Slow |
| @Test |
| public void testTimeoutOnQueuedRequest() throws Exception |
| { |
| long timeout = 1000; |
| start(new TimeoutHandler(3 * timeout)); |
| |
| // Only one connection so requests get queued |
| client.setMaxConnectionsPerDestination(1); |
| |
| // The first request has a long timeout |
| final CountDownLatch firstLatch = new CountDownLatch(1); |
| Request request = client.newRequest("localhost", connector.getLocalPort()) |
| .scheme(scheme) |
| .timeout(4 * timeout, TimeUnit.MILLISECONDS); |
| request.send(new Response.CompleteListener() |
| { |
| @Override |
| public void onComplete(Result result) |
| { |
| Assert.assertFalse(result.isFailed()); |
| firstLatch.countDown(); |
| } |
| }); |
| |
| // Second request has a short timeout and should fail in the queue |
| final CountDownLatch secondLatch = new CountDownLatch(1); |
| request = client.newRequest("localhost", connector.getLocalPort()) |
| .scheme(scheme) |
| .timeout(timeout, TimeUnit.MILLISECONDS); |
| request.send(new Response.CompleteListener() |
| { |
| @Override |
| public void onComplete(Result result) |
| { |
| Assert.assertTrue(result.isFailed()); |
| secondLatch.countDown(); |
| } |
| }); |
| |
| Assert.assertTrue(secondLatch.await(2 * timeout, TimeUnit.MILLISECONDS)); |
| // The second request must fail before the first request has completed |
| Assert.assertTrue(firstLatch.getCount() > 0); |
| Assert.assertTrue(firstLatch.await(5 * timeout, TimeUnit.MILLISECONDS)); |
| } |
| |
| @Slow |
| @Test |
| public void testTimeoutIsCancelledOnSuccess() throws Exception |
| { |
| long timeout = 1000; |
| start(new TimeoutHandler(timeout)); |
| |
| final CountDownLatch latch = new CountDownLatch(1); |
| final byte[] content = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; |
| Request request = client.newRequest("localhost", connector.getLocalPort()) |
| .scheme(scheme) |
| .content(new InputStreamContentProvider(new ByteArrayInputStream(content))) |
| .timeout(2 * timeout, TimeUnit.MILLISECONDS); |
| request.send(new BufferingResponseListener() |
| { |
| @Override |
| public void onComplete(Result result) |
| { |
| Assert.assertFalse(result.isFailed()); |
| Assert.assertArrayEquals(content, getContent()); |
| latch.countDown(); |
| } |
| }); |
| |
| Assert.assertTrue(latch.await(3 * timeout, TimeUnit.MILLISECONDS)); |
| |
| TimeUnit.MILLISECONDS.sleep(2 * timeout); |
| |
| Assert.assertNull(request.getAbortCause()); |
| } |
| |
| @Slow |
| @Test |
| public void testTimeoutOnListenerWithExplicitConnection() throws Exception |
| { |
| long timeout = 1000; |
| start(new TimeoutHandler(2 * timeout)); |
| |
| final CountDownLatch latch = new CountDownLatch(1); |
| Destination destination = client.getDestination(scheme, "localhost", connector.getLocalPort()); |
| FuturePromise<Connection> futureConnection = new FuturePromise<>(); |
| destination.newConnection(futureConnection); |
| try (Connection connection = futureConnection.get(5, TimeUnit.SECONDS)) |
| { |
| Request request = client.newRequest("localhost", connector.getLocalPort()) |
| .scheme(scheme) |
| .timeout(timeout, TimeUnit.MILLISECONDS); |
| connection.send(request, new Response.CompleteListener() |
| { |
| @Override |
| public void onComplete(Result result) |
| { |
| Assert.assertTrue(result.isFailed()); |
| latch.countDown(); |
| } |
| }); |
| |
| Assert.assertTrue(latch.await(3 * timeout, TimeUnit.MILLISECONDS)); |
| } |
| } |
| |
| @Slow |
| @Test |
| public void testTimeoutIsCancelledOnSuccessWithExplicitConnection() throws Exception |
| { |
| long timeout = 1000; |
| start(new TimeoutHandler(timeout)); |
| |
| final CountDownLatch latch = new CountDownLatch(1); |
| Destination destination = client.getDestination(scheme, "localhost", connector.getLocalPort()); |
| FuturePromise<Connection> futureConnection = new FuturePromise<>(); |
| destination.newConnection(futureConnection); |
| try (Connection connection = futureConnection.get(5, TimeUnit.SECONDS)) |
| { |
| Request request = client.newRequest(destination.getHost(), destination.getPort()) |
| .scheme(scheme) |
| .timeout(2 * timeout, TimeUnit.MILLISECONDS); |
| connection.send(request, new Response.CompleteListener() |
| { |
| @Override |
| public void onComplete(Result result) |
| { |
| Response response = result.getResponse(); |
| Assert.assertEquals(200, response.getStatus()); |
| Assert.assertFalse(result.isFailed()); |
| latch.countDown(); |
| } |
| }); |
| |
| Assert.assertTrue(latch.await(3 * timeout, TimeUnit.MILLISECONDS)); |
| |
| TimeUnit.MILLISECONDS.sleep(2 * timeout); |
| |
| Assert.assertNull(request.getAbortCause()); |
| } |
| } |
| |
| @Test |
| public void testIdleTimeout() throws Throwable |
| { |
| long timeout = 1000; |
| start(new TimeoutHandler(2 * timeout)); |
| client.stop(); |
| final AtomicBoolean sslIdle = new AtomicBoolean(); |
| client = new HttpClient(new HttpClientTransportOverHTTP() |
| { |
| @Override |
| public HttpDestination newHttpDestination(Origin origin) |
| { |
| return new HttpDestinationOverHTTP(getHttpClient(), origin) |
| { |
| @Override |
| protected ClientConnectionFactory newSslClientConnectionFactory(ClientConnectionFactory connectionFactory) |
| { |
| HttpClient client = getHttpClient(); |
| return new SslClientConnectionFactory(client.getSslContextFactory(), client.getByteBufferPool(), client.getExecutor(), connectionFactory) |
| { |
| @Override |
| protected SslConnection newSslConnection(ByteBufferPool byteBufferPool, Executor executor, EndPoint endPoint, SSLEngine engine) |
| { |
| return new SslConnection(byteBufferPool, executor, endPoint, engine) |
| { |
| @Override |
| protected boolean onReadTimeout() |
| { |
| sslIdle.set(true); |
| return super.onReadTimeout(); |
| } |
| }; |
| } |
| }; |
| } |
| }; |
| } |
| }, sslContextFactory); |
| client.setIdleTimeout(timeout); |
| client.start(); |
| |
| try |
| { |
| client.newRequest("localhost", connector.getLocalPort()) |
| .scheme(scheme) |
| .send(); |
| Assert.fail(); |
| } |
| catch (Exception x) |
| { |
| Assert.assertFalse(sslIdle.get()); |
| Assert.assertThat(x.getCause(), Matchers.instanceOf(TimeoutException.class)); |
| } |
| } |
| |
| @Slow |
| @Test |
| public void testBlockingConnectTimeoutFailsRequest() throws Exception |
| { |
| testConnectTimeoutFailsRequest(true); |
| } |
| |
| @Slow |
| @Test |
| public void testNonBlockingConnectTimeoutFailsRequest() throws Exception |
| { |
| testConnectTimeoutFailsRequest(false); |
| } |
| |
| private void testConnectTimeoutFailsRequest(boolean blocking) throws Exception |
| { |
| String host = "10.255.255.1"; |
| int port = 80; |
| int connectTimeout = 1000; |
| assumeConnectTimeout(host, port, connectTimeout); |
| |
| start(new EmptyServerHandler()); |
| client.stop(); |
| client.setConnectTimeout(connectTimeout); |
| client.setConnectBlocking(blocking); |
| client.start(); |
| |
| final CountDownLatch latch = new CountDownLatch(1); |
| Request request = client.newRequest(host, port); |
| request.scheme(scheme) |
| .send(new Response.CompleteListener() |
| { |
| @Override |
| public void onComplete(Result result) |
| { |
| if (result.isFailed()) |
| latch.countDown(); |
| } |
| }); |
| |
| Assert.assertTrue(latch.await(2 * connectTimeout, TimeUnit.MILLISECONDS)); |
| Assert.assertNotNull(request.getAbortCause()); |
| } |
| |
| @Slow |
| @Test |
| public void testConnectTimeoutIsCancelledByShorterRequestTimeout() throws Exception |
| { |
| String host = "10.255.255.1"; |
| int port = 80; |
| int connectTimeout = 2000; |
| assumeConnectTimeout(host, port, connectTimeout); |
| |
| start(new EmptyServerHandler()); |
| client.stop(); |
| client.setConnectTimeout(connectTimeout); |
| client.start(); |
| |
| final AtomicInteger completes = new AtomicInteger(); |
| final CountDownLatch latch = new CountDownLatch(2); |
| Request request = client.newRequest(host, port); |
| request.scheme(scheme) |
| .timeout(connectTimeout / 2, TimeUnit.MILLISECONDS) |
| .send(new Response.CompleteListener() |
| { |
| @Override |
| public void onComplete(Result result) |
| { |
| completes.incrementAndGet(); |
| latch.countDown(); |
| } |
| }); |
| |
| Assert.assertFalse(latch.await(2 * connectTimeout, TimeUnit.MILLISECONDS)); |
| Assert.assertEquals(1, completes.get()); |
| Assert.assertNotNull(request.getAbortCause()); |
| } |
| |
| @Test |
| public void retryAfterConnectTimeout() throws Exception |
| { |
| final String host = "10.255.255.1"; |
| final int port = 80; |
| int connectTimeout = 1000; |
| assumeConnectTimeout(host, port, connectTimeout); |
| |
| start(new EmptyServerHandler()); |
| client.stop(); |
| client.setConnectTimeout(connectTimeout); |
| client.start(); |
| |
| final CountDownLatch latch = new CountDownLatch(1); |
| Request request = client.newRequest(host, port); |
| request.scheme(scheme) |
| .send(new Response.CompleteListener() |
| { |
| @Override |
| public void onComplete(Result result) |
| { |
| if (result.isFailed()) |
| { |
| // Retry |
| client.newRequest(host, port) |
| .scheme(scheme) |
| .send(new Response.CompleteListener() |
| { |
| @Override |
| public void onComplete(Result result) |
| { |
| if (result.isFailed()) |
| latch.countDown(); |
| } |
| }); |
| } |
| } |
| }); |
| |
| Assert.assertTrue(latch.await(333 * connectTimeout, TimeUnit.MILLISECONDS)); |
| Assert.assertNotNull(request.getAbortCause()); |
| } |
| |
| @Test |
| public void testVeryShortTimeout() throws Exception |
| { |
| start(new EmptyServerHandler()); |
| |
| final CountDownLatch latch = new CountDownLatch(1); |
| client.newRequest("localhost", connector.getLocalPort()) |
| .scheme(scheme) |
| .timeout(1, TimeUnit.MILLISECONDS) // Very short timeout |
| .send(new Response.CompleteListener() |
| { |
| @Override |
| public void onComplete(Result result) |
| { |
| latch.countDown(); |
| } |
| }); |
| |
| Assert.assertTrue(latch.await(5, TimeUnit.SECONDS)); |
| } |
| |
| @Test |
| public void testTimeoutCancelledWhenSendingThrowsException() throws Exception |
| { |
| start(new EmptyServerHandler()); |
| |
| long timeout = 1000; |
| Request request = client.newRequest("badscheme://localhost:badport"); |
| |
| try |
| { |
| request.timeout(timeout, TimeUnit.MILLISECONDS) |
| .send(new Response.CompleteListener() |
| { |
| @Override |
| public void onComplete(Result result) |
| { |
| } |
| }); |
| Assert.fail(); |
| } |
| catch (Exception expected) |
| { |
| } |
| |
| Thread.sleep(2 * timeout); |
| |
| // If the task was not cancelled, it aborted the request. |
| Assert.assertNull(request.getAbortCause()); |
| } |
| |
| private void assumeConnectTimeout(String host, int port, int connectTimeout) throws IOException |
| { |
| try (Socket socket = new Socket()) |
| { |
| // Try to connect to a private address in the 10.x.y.z range. |
| // These addresses are usually not routed, so an attempt to |
| // connect to them will hang the connection attempt, which is |
| // what we want to simulate in this test. |
| socket.connect(new InetSocketAddress(host, port), connectTimeout); |
| // Abort the test if we can connect. |
| Assume.assumeTrue(false); |
| } |
| catch (SocketTimeoutException x) |
| { |
| // Expected timeout during connect, continue the test. |
| Assume.assumeTrue(true); |
| } |
| catch (Throwable x) |
| { |
| // Abort if any other exception happens. |
| Assume.assumeTrue(false); |
| } |
| } |
| |
| private class TimeoutHandler extends AbstractHandler |
| { |
| private final long timeout; |
| |
| public TimeoutHandler(long timeout) |
| { |
| this.timeout = timeout; |
| } |
| |
| @Override |
| public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException |
| { |
| baseRequest.setHandled(true); |
| try |
| { |
| TimeUnit.MILLISECONDS.sleep(timeout); |
| IO.copy(request.getInputStream(), response.getOutputStream()); |
| } |
| catch (InterruptedException x) |
| { |
| throw new ServletException(x); |
| } |
| } |
| } |
| } |