Skip to content

Instantly share code, notes, and snippets.

@Mahoney
Last active December 11, 2015 11:48
Show Gist options
  • Save Mahoney/4596702 to your computer and use it in GitHub Desktop.
Save Mahoney/4596702 to your computer and use it in GitHub Desktop.
Micro benchmark for comparing performance characteristics of different strategies for using an HttpClient to do concurrent requests to the same server.
import com.github.tomakehurst.wiremock.WireMockServer;
import org.apache.http.client.CookieStore;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.params.HttpClientParams;
import org.apache.http.cookie.Cookie;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.PoolingClientConnectionManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.head;
import static com.github.tomakehurst.wiremock.client.WireMock.reset;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
public class HttpClientConcurrentPerformanceTest {
private static final int WIREMOCK_PORT = 8080;
private static final int DELAY_IN_REMOTE_SERVICE = 100;
private static final String REQUEST_PATH = "/something";
private static final int REQUESTS_PER_CLIENT = 600;
private static CookieStore noOpCookieStore = new CookieStore() {
@Override
public void addCookie(Cookie cookie) {
}
@Override
public List<Cookie> getCookies() {
return Collections.emptyList();
}
@Override
public boolean clearExpired(Date date) {
return false;
}
@Override
public void clear() {
}
};
public static void main(String[] args) throws Exception {
WireMockServer wireMockServer = new WireMockServer(WIREMOCK_PORT);
wireMockServer.start();
try {
benchMarkWith(5);
benchMarkWith(10);
benchMarkWith(20);
benchMarkWith(50);
benchMarkWith(100);
} finally {
wireMockServer.stop();
}
}
private static void benchMarkWith(int concurrentRequests) throws Exception {
reportResults(concurrentRequests, singleClient(concurrentRequests), "Single client");
reportResults(concurrentRequests, clientPerThread(concurrentRequests), "Client per thread");
reportResults(concurrentRequests, clientPerRequest(concurrentRequests), "Client per request");
}
private static void reportResults(int concurrentRequests, long elapsedTimeNanos, String testDescription) {
double latencyNanos = elapsedTimeNanos / REQUESTS_PER_CLIENT;
double latencyMillis = latencyNanos / 1000000F;
double finalLatency = latencyMillis - DELAY_IN_REMOTE_SERVICE;
System.out.println(testDescription+" with "+concurrentRequests+" threads. Average latency per request: "+String.format("%.3g", finalLatency)+" milliseconds.");
}
private static void setUpWiremock() {
reset();
stubFor(head(urlMatching(REQUEST_PATH)).willReturn(
aResponse()
.withFixedDelay(DELAY_IN_REMOTE_SERVICE)
.withStatus(200)
.withHeader("Header1", "value1")
.withHeader("Header2", "value2")
.withHeader("Header3", "value3")
.withHeader("Header4", "value4")
));
}
private static long clientPerRequest(int concurrentRequests) throws Exception {
final HttpClientSource httpClientSource = new HttpClientSource() {
@Override
public HttpClient get() {
return nonThreadsafeHttpClient();
}
@Override
public void release(HttpClient httpClient) {
httpClient.getConnectionManager().shutdown();
}
};
return runTest(httpClientSource, concurrentRequests, "Client per request");
}
private static long singleClient(int concurrentRequests) throws Exception {
final HttpClient httpClient = threadsafeHttpClient(concurrentRequests);
final HttpClientSource httpClientSource = new HttpClientSource() {
@Override
public HttpClient get() {
return httpClient;
}
@Override
public void release(HttpClient httpClient) {}
};
final long result = runTest(httpClientSource, concurrentRequests, "Single client");
httpClient.getConnectionManager().shutdown();
return result;
}
private static long clientPerThread(int concurrentRequests) throws Exception {
final ThreadLocal<DefaultHttpClient> httpClient = new ThreadLocal<DefaultHttpClient>();
final HttpClientSource httpClientSource = new HttpClientSource() {
@Override
public HttpClient get() {
DefaultHttpClient defaultHttpClient = httpClient.get();
if (defaultHttpClient == null) {
defaultHttpClient = nonThreadsafeHttpClient();
defaultHttpClient.setCookieStore(noOpCookieStore);
httpClient.set(defaultHttpClient);
}
return defaultHttpClient;
}
@Override
public void release(HttpClient httpClient) {}
};
return runTest(httpClientSource, concurrentRequests, "Client per thread");
}
private static long runTest(final HttpClientSource httpClientSource, int concurrentRequests, final String testType) throws Exception {
final List<Thread> threads = new ArrayList<Thread>();
final List<DefaultUncaughtExceptionHandler> uncaughtExceptionHandlers = new ArrayList<DefaultUncaughtExceptionHandler>();
final CountDownLatch allReadyToStart = new CountDownLatch(1);
final CountDownLatch warmedUp = new CountDownLatch(concurrentRequests);
setUpWiremock();
for (int i = 0; i < concurrentRequests; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
try {
runRequests(2, "warm up");
} finally {
warmedUp.countDown();
}
allReadyToStart.await();
runRequests(REQUESTS_PER_CLIENT, "actual test");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private void runRequests(int requestsPerClient, String description) throws Exception {
for (int i = 0; i < requestsPerClient; i++) {
HttpHead request = new HttpHead("http://localhost:"+WIREMOCK_PORT+REQUEST_PATH);
HttpClient httpClient = httpClientSource.get();
try {
httpClient.execute(request);
} catch (Exception e) {
throw new RuntimeException("Failed to make request "+i+" of "+requestsPerClient+" doing "+description+" for "+testType, e);
} finally {
request.abort();
httpClientSource.release(httpClient);
}
}
}
});
final DefaultUncaughtExceptionHandler exceptionHandler = new DefaultUncaughtExceptionHandler();
thread.setUncaughtExceptionHandler(exceptionHandler);
uncaughtExceptionHandlers.add(exceptionHandler);
threads.add(thread);
thread.start();
}
warmedUp.await();
setUpWiremock();
long start = System.nanoTime();
allReadyToStart.countDown();
for (Thread thread: threads) {
thread.join();
}
final long elapsed = System.nanoTime() - start;
boolean success = true;
for (DefaultUncaughtExceptionHandler exceptionHandler: uncaughtExceptionHandlers) {
final boolean threadSuccess = exceptionHandler.isSuccess();
if (!threadSuccess) {
exceptionHandler.print();
}
success = (success && threadSuccess);
}
if (success) {
return elapsed;
} else {
throw new Exception("Failed to run test...");
}
}
private static DefaultHttpClient nonThreadsafeHttpClient() {
HttpParams params = basicParams();
return new DefaultHttpClient(params);
}
private static DefaultHttpClient threadsafeHttpClient(int concurrentRequests) {
HttpParams params = basicParams();
PoolingClientConnectionManager connectionManager = new PoolingClientConnectionManager();
connectionManager.setDefaultMaxPerRoute(concurrentRequests);
connectionManager.setMaxTotal(concurrentRequests);
final DefaultHttpClient defaultHttpClient = new DefaultHttpClient(connectionManager, params);
defaultHttpClient.setCookieStore(noOpCookieStore);
return defaultHttpClient;
}
private static HttpParams basicParams() {
HttpParams params = new BasicHttpParams();
HttpConnectionParams.setSoTimeout(params, 120000);
HttpConnectionParams.setConnectionTimeout(params, 120000);
HttpClientParams.setRedirecting(params, false);
HttpClientParams.setConnectionManagerTimeout(params, 120000);
return params;
}
private static class DefaultUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
private volatile boolean success = true;
private volatile Thread thread = null;
private volatile Throwable throwable = null;
@Override
public void uncaughtException(Thread t, Throwable e) {
this.success = false;
this.thread = t;
this.throwable = e;
}
public boolean isSuccess() {
return success;
}
public void print() {
if (!success) {
throwable.printStackTrace();
}
}
}
private static interface HttpClientSource {
HttpClient get();
void release(HttpClient httpClient);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment