blob: 3457050de4af76cbdef557a20307b07ce9a8d157 [file] [log] [blame]
//
// ========================================================================
// 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);
}
}
}
}