diff options
Diffstat (limited to 'tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientLoadTest.java')
-rw-r--r-- | tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientLoadTest.java | 356 |
1 files changed, 356 insertions, 0 deletions
diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientLoadTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientLoadTest.java new file mode 100644 index 0000000000..0fab406c2f --- /dev/null +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientLoadTest.java @@ -0,0 +1,356 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 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.http.client; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.client.ConnectionPool; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpClientTransport; +import org.eclipse.jetty.client.HttpDestination; +import org.eclipse.jetty.client.LeakTrackingConnectionPool; +import org.eclipse.jetty.client.Origin; +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.BytesContentProvider; +import org.eclipse.jetty.fcgi.client.http.HttpClientTransportOverFCGI; +import org.eclipse.jetty.fcgi.client.http.HttpDestinationOverFCGI; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.io.ArrayByteBufferPool; +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.LeakTrackingByteBufferPool; +import org.eclipse.jetty.io.MappedByteBufferPool; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.LeakDetector; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.thread.Scheduler; +import org.hamcrest.Matchers; +import org.junit.Assert; +import org.junit.Test; + +import static org.junit.Assert.assertThat; + +public class HttpClientLoadTest extends AbstractTest +{ + private final Logger logger = Log.getLogger(HttpClientLoadTest.class); + private final AtomicLong connectionLeaks = new AtomicLong(); + + public HttpClientLoadTest(Transport transport) + { + super(transport); + } + + @Override + protected ServerConnector newServerConnector(Server server) + { + int cores = Runtime.getRuntime().availableProcessors(); + ByteBufferPool byteBufferPool = new ArrayByteBufferPool(); + byteBufferPool = new LeakTrackingByteBufferPool(byteBufferPool); + return new ServerConnector(server, null, null, byteBufferPool, + 1, Math.min(1, cores / 2), provideServerConnectionFactory(transport)); + } + + @Override + protected HttpClientTransport provideClientTransport(Transport transport) + { + switch (transport) + { + case HTTP: + case HTTPS: + { + return new HttpClientTransportOverHTTP(1) + { + @Override + public HttpDestination newHttpDestination(Origin origin) + { + return new HttpDestinationOverHTTP(getHttpClient(), origin) + { + @Override + protected ConnectionPool newConnectionPool(HttpClient client) + { + return new LeakTrackingConnectionPool(this, client.getMaxConnectionsPerDestination(), this) + { + @Override + protected void leaked(LeakDetector.LeakInfo leakInfo) + { + super.leaked(leakInfo); + connectionLeaks.incrementAndGet(); + } + }; + } + }; + } + }; + } + case FCGI: + { + return new HttpClientTransportOverFCGI(1, false, "") + { + @Override + public HttpDestination newHttpDestination(Origin origin) + { + return new HttpDestinationOverFCGI(getHttpClient(), origin) + { + @Override + protected ConnectionPool newConnectionPool(HttpClient client) + { + return new LeakTrackingConnectionPool(this, client.getMaxConnectionsPerDestination(), this) + { + @Override + protected void leaked(LeakDetector.LeakInfo leakInfo) + { + super.leaked(leakInfo); + connectionLeaks.incrementAndGet(); + } + }; + } + }; + } + }; + } + default: + { + return super.provideClientTransport(transport); + } + } + } + + @Test + public void testIterative() throws Exception + { + start(new LoadHandler()); + + client.setByteBufferPool(new LeakTrackingByteBufferPool(new MappedByteBufferPool.Tagged())); + client.setMaxConnectionsPerDestination(32768); + client.setMaxRequestsQueuedPerDestination(1024 * 1024); + + Random random = new Random(); + // At least 25k requests to warmup properly (use -XX:+PrintCompilation to verify JIT activity) + int runs = 1; + int iterations = 500; + for (int i = 0; i < runs; ++i) + { + run(random, iterations); + } + + // Re-run after warmup + iterations = 5_000; + for (int i = 0; i < runs; ++i) + { + run(random, iterations); + } + + System.gc(); + + ByteBufferPool byteBufferPool = connector.getByteBufferPool(); + if (byteBufferPool instanceof LeakTrackingByteBufferPool) + { + LeakTrackingByteBufferPool serverBufferPool = (LeakTrackingByteBufferPool)byteBufferPool; + assertThat("Server BufferPool - leaked acquires", serverBufferPool.getLeakedAcquires(), Matchers.is(0L)); + assertThat("Server BufferPool - leaked releases", serverBufferPool.getLeakedReleases(), Matchers.is(0L)); + assertThat("Server BufferPool - unreleased", serverBufferPool.getLeakedResources(), Matchers.is(0L)); + } + + byteBufferPool = client.getByteBufferPool(); + if (byteBufferPool instanceof LeakTrackingByteBufferPool) + { + LeakTrackingByteBufferPool clientBufferPool = (LeakTrackingByteBufferPool)byteBufferPool; + assertThat("Client BufferPool - leaked acquires", clientBufferPool.getLeakedAcquires(), Matchers.is(0L)); + assertThat("Client BufferPool - leaked releases", clientBufferPool.getLeakedReleases(), Matchers.is(0L)); + assertThat("Client BufferPool - unreleased", clientBufferPool.getLeakedResources(), Matchers.is(0L)); + } + + assertThat("Connection Leaks", connectionLeaks.get(), Matchers.is(0L)); + } + + private void run(Random random, int iterations) throws InterruptedException + { + CountDownLatch latch = new CountDownLatch(iterations); + List<String> failures = new ArrayList<>(); + + int factor = (logger.isDebugEnabled() ? 25 : 1) * 100; + + // Dumps the state of the client if the test takes too long + final Thread testThread = Thread.currentThread(); + Scheduler.Task task = client.getScheduler().schedule(() -> + { + logger.warn("Interrupting test, it is taking too long"); + logger.warn(client.dump()); + testThread.interrupt(); + }, iterations * factor, TimeUnit.MILLISECONDS); + + long begin = System.nanoTime(); + for (int i = 0; i < iterations; ++i) + { + test(random, latch, failures); +// test("http", "localhost", "GET", false, false, 64 * 1024, false, latch, failures); + } + Assert.assertTrue(latch.await(iterations, TimeUnit.SECONDS)); + long end = System.nanoTime(); + task.cancel(); + long elapsed = TimeUnit.NANOSECONDS.toMillis(end - begin); + logger.info("{} requests in {} ms, {} req/s", iterations, elapsed, elapsed > 0 ? iterations * 1000 / elapsed : -1); + + for (String failure : failures) + System.err.println("FAILED: "+failure); + + Assert.assertTrue(failures.toString(), failures.isEmpty()); + } + + private void test(Random random, final CountDownLatch latch, final List<String> failures) throws InterruptedException + { + // Choose a random destination + String host = random.nextBoolean() ? "localhost" : "127.0.0.1"; + // Choose a random method + HttpMethod method = random.nextBoolean() ? HttpMethod.GET : HttpMethod.POST; + + boolean ssl = isTransportSecure(); + + // Choose randomly whether to close the connection on the client or on the server + boolean clientClose = false; + if (!ssl && random.nextBoolean()) + clientClose = true; + boolean serverClose = false; + if (!ssl && random.nextBoolean()) + serverClose = true; + + int maxContentLength = 64 * 1024; + int contentLength = random.nextInt(maxContentLength) + 1; + + test(ssl ? "https" : "http", host, method.asString(), clientClose, serverClose, contentLength, true, latch, failures); + } + + private void test(String scheme, String host, String method, boolean clientClose, boolean serverClose, int contentLength, final boolean checkContentLength, final CountDownLatch latch, final List<String> failures) throws InterruptedException + { + Request request = client.newRequest(host, connector.getLocalPort()) + .scheme(scheme) + .method(method); + + if (clientClose) + request.header(HttpHeader.CONNECTION, "close"); + else if (serverClose) + request.header("X-Close", "true"); + + switch (method) + { + case "GET": + request.header("X-Download", String.valueOf(contentLength)); + break; + case "POST": + request.header("X-Upload", String.valueOf(contentLength)); + request.content(new BytesContentProvider(new byte[contentLength])); + break; + } + + final CountDownLatch requestLatch = new CountDownLatch(1); + request.send(new Response.Listener.Adapter() + { + private final AtomicInteger contentLength = new AtomicInteger(); + + @Override + public void onHeaders(Response response) + { + if (checkContentLength) + { + String content = response.getHeaders().get("X-Content"); + if (content != null) + contentLength.set(Integer.parseInt(content)); + } + } + + @Override + public void onContent(Response response, ByteBuffer content) + { + if (checkContentLength) + contentLength.addAndGet(-content.remaining()); + } + + @Override + public void onComplete(Result result) + { + if (result.isFailed()) + { + result.getFailure().printStackTrace(); + failures.add("Result failed " + result); + } + + if (checkContentLength && contentLength.get() != 0) + failures.add("Content length mismatch " + contentLength); + + requestLatch.countDown(); + latch.countDown(); + } + }); + requestLatch.await(5, TimeUnit.SECONDS); + } + + private class LoadHandler extends AbstractHandler + { + @Override + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + String method = request.getMethod().toUpperCase(Locale.ENGLISH); + switch (method) + { + case "GET": + { + int contentLength = request.getIntHeader("X-Download"); + if (contentLength > 0) + { + response.setHeader("X-Content", String.valueOf(contentLength)); + response.getOutputStream().write(new byte[contentLength]); + } + break; + } + case "POST": + { + response.setHeader("X-Content", request.getHeader("X-Upload")); + IO.copy(request.getInputStream(), response.getOutputStream()); + break; + } + } + + if (Boolean.parseBoolean(request.getHeader("X-Close"))) + response.setHeader("Connection", "close"); + + baseRequest.setHandled(true); + } + } +} |