summaryrefslogtreecommitdiffstats
path: root/clustercontroller-apputil
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
committerJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
commit72231250ed81e10d66bfe70701e64fa5fe50f712 (patch)
tree2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /clustercontroller-apputil
Publish
Diffstat (limited to 'clustercontroller-apputil')
-rw-r--r--clustercontroller-apputil/.gitignore2
-rw-r--r--clustercontroller-apputil/OWNERS2
-rw-r--r--clustercontroller-apputil/pom.xml67
-rw-r--r--clustercontroller-apputil/src/main/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/ApacheAsyncHttpClient.java179
-rw-r--r--clustercontroller-apputil/src/main/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/ApacheHttpInstance.java129
-rw-r--r--clustercontroller-apputil/src/main/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/JDiscHttpRequestHandler.java127
-rw-r--r--clustercontroller-apputil/src/main/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/JDiscMetricWrapper.java50
-rw-r--r--clustercontroller-apputil/src/main/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/package-info.java5
-rw-r--r--clustercontroller-apputil/src/test/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/ApacheAsyncHttpClientTest.java239
-rw-r--r--clustercontroller-apputil/src/test/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/JDiscHttpRequestHandlerTest.java47
-rw-r--r--clustercontroller-apputil/src/test/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/JDiscMetricWrapperTest.java44
11 files changed, 891 insertions, 0 deletions
diff --git a/clustercontroller-apputil/.gitignore b/clustercontroller-apputil/.gitignore
new file mode 100644
index 00000000000..12251442258
--- /dev/null
+++ b/clustercontroller-apputil/.gitignore
@@ -0,0 +1,2 @@
+/target
+/pom.xml.build
diff --git a/clustercontroller-apputil/OWNERS b/clustercontroller-apputil/OWNERS
new file mode 100644
index 00000000000..b3db17e22d8
--- /dev/null
+++ b/clustercontroller-apputil/OWNERS
@@ -0,0 +1,2 @@
+vekterli
+hakon
diff --git a/clustercontroller-apputil/pom.xml b/clustercontroller-apputil/pom.xml
new file mode 100644
index 00000000000..261757bbf6f
--- /dev/null
+++ b/clustercontroller-apputil/pom.xml
@@ -0,0 +1,67 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>parent</artifactId>
+ <version>6-SNAPSHOT</version>
+ <relativePath>../parent/pom.xml</relativePath>
+ </parent>
+ <artifactId>clustercontroller-apputil</artifactId>
+ <version>6-SNAPSHOT</version>
+ <packaging>container-plugin</packaging>
+ <dependencies>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>annotations</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>container-dev</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>clustercontroller-utils</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ <version>2.4</version>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <compilerArgs>
+ <arg>-Xlint:unchecked</arg>
+ <arg>-Xlint:deprecation</arg>
+ </compilerArgs>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/clustercontroller-apputil/src/main/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/ApacheAsyncHttpClient.java b/clustercontroller-apputil/src/main/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/ApacheAsyncHttpClient.java
new file mode 100644
index 00000000000..f78d546b71c
--- /dev/null
+++ b/clustercontroller-apputil/src/main/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/ApacheAsyncHttpClient.java
@@ -0,0 +1,179 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.clustercontroller.apputil.communication.http;
+
+import com.yahoo.vespa.clustercontroller.utils.communication.async.AsyncOperation;
+import com.yahoo.vespa.clustercontroller.utils.communication.async.AsyncOperationImpl;
+import com.yahoo.vespa.clustercontroller.utils.communication.http.AsyncHttpClient;
+import com.yahoo.vespa.clustercontroller.utils.communication.http.HttpRequest;
+import com.yahoo.vespa.clustercontroller.utils.communication.http.HttpResult;
+import com.yahoo.vespa.clustercontroller.utils.communication.http.SyncHttpClient;
+
+import java.util.*;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+/**
+ * There are some stuff to work around with the apache client.
+ * - Whether to use a proxy or not is global, not per request.
+ * - Timeout is not handled per request (and is not a request timeout but seems like a "something happening on TCP" timeout.
+ * - It is not thread safe.
+ *
+ * This class gets around these issues by creating one instance per unique setting, and ensuring only one request use a given instance at a time.
+ */
+public class ApacheAsyncHttpClient implements AsyncHttpClient<HttpResult> {
+ private static final Logger log = Logger.getLogger(ApacheAsyncHttpClient.class.getName());
+ public interface SyncHttpClientFactory {
+ SyncHttpClient createInstance(String proxyHost, int proxyPort, long timeoutMs);
+ }
+ public static class Settings {
+ String proxyHost;
+ int proxyPort;
+ long timeout;
+
+ Settings(HttpRequest request) {
+ timeout = request.getTimeoutMillis();
+ if (request.getPath() != null
+ && !request.getPath().isEmpty()
+ && request.getPath().charAt(0) != '/')
+ {
+ proxyHost = request.getHost();
+ proxyPort = request.getPort();
+ int colo = request.getPath().indexOf(':');
+ int slash = request.getPath().indexOf('/', colo);
+ if (colo < 0 && slash < 0) {
+ throw new IllegalStateException("Http path '" + request.getPath() + "' looks invalid. "
+ + "Cannot extract proxy server data. Is it a regular request that "
+ + "should start with a slash?");
+ }
+ if (colo < 0) {
+ request.setPort(80);
+ request.setHost(request.getPath().substring(0, slash));
+ } else {
+ request.setHost(request.getPath().substring(0, colo));
+ request.setPort(Integer.valueOf(request.getPath().substring(colo + 1, slash)));
+ }
+ request.setPath(request.getPath().substring(slash));
+ }
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ Settings o = (Settings) other;
+ if (timeout != o.timeout || proxyPort != o.proxyPort
+ || (proxyHost == null ^ o.proxyHost == null)
+ || (proxyHost != null && !proxyHost.equals(o.proxyHost)))
+ {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return (proxyHost == null ? 0 : proxyHost.hashCode()) ^ proxyPort ^ Long.valueOf(timeout).hashCode();
+ }
+ }
+ private final Executor executor;
+ private final SyncHttpClientFactory clientFactory;
+ private boolean closed = false;
+ private final int maxInstanceCacheSize; // Maximum number of currently unused instances.
+ private final Map<Settings, LinkedList<SyncHttpClient>> apacheInstances = new LinkedHashMap<Settings, LinkedList<SyncHttpClient>>() {
+ protected boolean removeEldestEntry(Map.Entry eldest) {
+ return getUnusedCacheSize() > maxInstanceCacheSize;
+ }
+ };
+
+ public ApacheAsyncHttpClient(Executor executor) {
+ this(executor, new SyncHttpClientFactory() {
+ @Override
+ public SyncHttpClient createInstance(String proxyHost, int proxyPort, long timeoutMs) {
+ return new ApacheHttpInstance(proxyHost, proxyPort, timeoutMs);
+ }
+ });
+ }
+
+ public ApacheAsyncHttpClient(Executor executor, SyncHttpClientFactory clientFactory) {
+ this.executor = executor;
+ this.clientFactory = clientFactory;
+ maxInstanceCacheSize = 16;
+ log.fine("Starting apache com.yahoo.vespa.clustercontroller.utils.communication.async HTTP client");
+ }
+
+ private SyncHttpClient getFittingInstance(Settings settings) {
+ synchronized (apacheInstances) {
+ if (closed) throw new IllegalStateException("Http client has been closed for business.");
+ LinkedList<SyncHttpClient> fittingInstances = apacheInstances.get(settings);
+ if (fittingInstances == null) {
+ fittingInstances = new LinkedList<>();
+ apacheInstances.put(settings, fittingInstances);
+ }
+ if (fittingInstances.isEmpty()) {
+ return clientFactory.createInstance(settings.proxyHost, settings.proxyPort, settings.timeout);
+ } else {
+ return fittingInstances.removeFirst();
+ }
+ }
+ }
+ private void insertInstance(Settings settings, SyncHttpClient instance) {
+ synchronized (apacheInstances) {
+ LinkedList<SyncHttpClient> fittingInstances = apacheInstances.get(settings);
+ if (closed || fittingInstances == null) {
+ instance.close();
+ return;
+ }
+ fittingInstances.addLast(instance);
+ }
+ }
+ private int getUnusedCacheSize() {
+ int size = 0;
+ synchronized (apacheInstances) {
+ for (LinkedList<SyncHttpClient> list : apacheInstances.values()) {
+ size += list.size();
+ }
+ }
+ return size;
+ }
+
+ @Override
+ public AsyncOperation<HttpResult> execute(HttpRequest r) {
+ final HttpRequest request = r.clone(); // Gonna modify it to extract proxy information
+ final Settings settings = new Settings(request);
+ final SyncHttpClient instance = getFittingInstance(settings);
+ final AsyncOperationImpl<HttpResult> op = new AsyncOperationImpl<>(r.toString(), r.toString(true));
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ HttpResult result;
+ Exception failure = null;
+ try{
+ result = instance.execute(request);
+ } catch (Exception e) {
+ result = new HttpResult().setHttpCode(500, "Apache client failed to execute request.");
+ failure = e;
+ }
+ insertInstance(settings, instance);
+ // Must insert back instance before tagging operation complete to ensure a following
+ // call can reuse same instance
+ if (failure != null) {
+ op.setFailure(failure, result);
+ } else {
+ op.setResult(result);
+ }
+ }
+ });
+ return op;
+ }
+
+ @Override
+ public void close() {
+ synchronized (apacheInstances) {
+ closed = true;
+ for (LinkedList<SyncHttpClient> list : apacheInstances.values()) {
+ for (SyncHttpClient instance : list) {
+ instance.close();
+ }
+ }
+ apacheInstances.clear();
+ }
+ }
+}
diff --git a/clustercontroller-apputil/src/main/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/ApacheHttpInstance.java b/clustercontroller-apputil/src/main/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/ApacheHttpInstance.java
new file mode 100644
index 00000000000..324d6ff3b85
--- /dev/null
+++ b/clustercontroller-apputil/src/main/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/ApacheHttpInstance.java
@@ -0,0 +1,129 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.clustercontroller.apputil.communication.http;
+
+import com.yahoo.vespa.clustercontroller.utils.communication.http.HttpRequest;
+import com.yahoo.vespa.clustercontroller.utils.communication.http.HttpResult;
+import com.yahoo.vespa.clustercontroller.utils.communication.http.SyncHttpClient;
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.conn.params.ConnRoutePNames;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+import org.apache.http.util.EntityUtils;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.logging.Logger;
+
+/**
+ * Synchronous http client using Apache commons.
+ */
+public class ApacheHttpInstance implements SyncHttpClient {
+ private static final Logger log = Logger.getLogger(ApacheHttpInstance.class.getName());
+ DefaultHttpClient client;
+
+ public ApacheHttpInstance(String proxyHost, int proxyPort, long timeoutMs) {
+ if (timeoutMs > Integer.MAX_VALUE) throw new IllegalArgumentException("Cannot handle timeout not contained in an integer");
+ HttpParams httpParams = new BasicHttpParams();
+ HttpConnectionParams.setConnectionTimeout(httpParams, (int) timeoutMs);
+ HttpConnectionParams.setSoTimeout(httpParams, (int) timeoutMs);
+
+ if (proxyHost != null && !proxyHost.isEmpty()) {
+ httpParams.setParameter(ConnRoutePNames.DEFAULT_PROXY, new HttpHost(proxyHost, proxyPort, "http"));
+ }
+
+ client = new DefaultHttpClient(httpParams);
+ }
+
+ /** This function is not threadsafe. */
+ public HttpResult execute(HttpRequest r) {
+ HttpRequest.HttpOp op = r.getHttpOperation();
+ if (op == null) {
+ if (r.getPostContent() != null) {
+ log.fine("Request " + r + " has no HTTP function specified. Assuming POST as post content is set.");
+ op = HttpRequest.HttpOp.POST;
+ } else {
+ log.fine("Request " + r + " has no HTTP function specified. Assuming GET as post content is set.");
+ op = HttpRequest.HttpOp.GET;
+ }
+ }
+ if (r.getPostContent() != null
+ && !(op.equals(HttpRequest.HttpOp.POST) || op.equals(HttpRequest.HttpOp.PUT)))
+ {
+ throw new IllegalStateException("A " + op + " operation can't have content");
+ }
+ try {
+ HttpHost target = new HttpHost(r.getHost(), r.getPort(), "http");
+ org.apache.http.HttpRequest req = null;
+
+ String path = r.getPath();
+ int uriOption = 0;
+ for (HttpRequest.KeyValuePair option : r.getUrlOptions()) {
+ path += (++uriOption == 1 ? '?' : '&');
+ path += option.getKey() + '=' + option.getValue();
+ }
+
+ switch (op) {
+ case POST:
+ HttpPost post = new HttpPost(path);
+ if (r.getPostContent() != null) {
+ post.setEntity(new StringEntity(r.getPostContent().toString()));
+ }
+ req = post;
+ break;
+ case GET:
+ req = new HttpGet(path);
+ break;
+ case PUT:
+ HttpPut put = new HttpPut(path);
+ put.setEntity(new StringEntity(r.getPostContent().toString()));
+ req = put;
+ break;
+ case DELETE:
+ req = new HttpDelete(path);
+ break;
+ }
+
+ for (HttpRequest.KeyValuePair header : r.getHeaders()) {
+ req.addHeader(header.getKey(), header.getValue());
+ }
+
+ HttpResponse rsp = client.execute(target, req);
+ HttpEntity entity = rsp.getEntity();
+
+ HttpResult result = new HttpResult();
+ result.setHttpCode(rsp.getStatusLine().getStatusCode(), rsp.getStatusLine().getReasonPhrase());
+
+ if (entity != null) {
+ result.setContent(EntityUtils.toString(entity));
+ }
+ for (Header header : rsp.getAllHeaders()) {
+ result.addHeader(header.getName(), header.getValue());
+ }
+
+ return result;
+ } catch (Exception e) {
+ HttpResult result = new HttpResult();
+
+ StringWriter writer = new StringWriter();
+ e.printStackTrace(new PrintWriter(writer));
+ writer.flush();
+
+ result.setHttpCode(500, "Got exception " + writer.toString() + " when sending message.");
+ return result;
+ }
+ }
+
+ public void close() {
+ client.getConnectionManager().shutdown();
+ }
+}
diff --git a/clustercontroller-apputil/src/main/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/JDiscHttpRequestHandler.java b/clustercontroller-apputil/src/main/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/JDiscHttpRequestHandler.java
new file mode 100644
index 00000000000..5c09c461a26
--- /dev/null
+++ b/clustercontroller-apputil/src/main/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/JDiscHttpRequestHandler.java
@@ -0,0 +1,127 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.clustercontroller.apputil.communication.http;
+
+import com.yahoo.container.jdisc.LoggingRequestHandler;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.jdisc.HeaderFields;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.handler.CompletionHandler;
+import com.yahoo.text.Utf8;
+import com.yahoo.vespa.clustercontroller.utils.communication.http.HttpRequest;
+import com.yahoo.vespa.clustercontroller.utils.communication.http.HttpRequestHandler;
+import com.yahoo.vespa.clustercontroller.utils.communication.http.HttpResult;
+import org.apache.commons.io.IOUtils;
+
+import java.io.*;
+import java.time.Duration;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+/**
+ * Note. This class is tested through apache http instance test, using this as other endpoint.
+ * @author humbe
+ * @author Harald Musum
+ * @author Vegard Sjonfjell
+ */
+public class JDiscHttpRequestHandler extends LoggingRequestHandler {
+
+ private static final Logger log = Logger.getLogger(JDiscHttpRequestHandler.class.getName());
+ private final HttpRequestHandler requestHandler;
+
+ public JDiscHttpRequestHandler(HttpRequestHandler handler, Executor executor, AccessLog accessLog) {
+ super(executor, accessLog);
+ this.requestHandler = handler;
+ }
+
+ static class EmptyCompletionHandler implements CompletionHandler {
+ @Override
+ public void completed() { }
+ @Override
+ public void failed(Throwable throwable) { }
+ }
+
+ @Override
+ public com.yahoo.container.jdisc.HttpResponse handle(com.yahoo.container.jdisc.HttpRequest request) {
+ final HttpRequest legacyRequest = new HttpRequest();
+ final com.yahoo.jdisc.http.HttpRequest jDiscRequest = request.getJDiscRequest();
+
+ legacyRequest.setHost(request.getUri().getHost());
+ setOperation(legacyRequest, request.getMethod());
+ legacyRequest.setPort(request.getUri().getPort());
+ legacyRequest.setPath(request.getUri().getPath());
+ copyPostData(request, legacyRequest);
+ copyRequestHeaders(legacyRequest, jDiscRequest);
+ copyParameters(legacyRequest, jDiscRequest);
+ legacyRequest.setTimeout(Duration.ofMinutes(60).toMillis());
+
+ try {
+ final HttpResult result = requestHandler.handleRequest(legacyRequest);
+ log.fine("Got result " + result.toString(true));
+ return copyResponse(result);
+ } catch (Exception e) {
+ log.warning("Caught exception while handling request: " + e.getMessage());
+ return new com.yahoo.container.jdisc.HttpResponse(500) {
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ outputStream.write(Utf8.toBytes(e.getMessage()));
+ }
+ };
+ }
+ }
+
+ static HttpRequest setOperation(HttpRequest request, com.yahoo.jdisc.http.HttpRequest.Method method) {
+ switch (method) {
+ case GET: return request.setHttpOperation(HttpRequest.HttpOp.GET);
+ case POST: return request.setHttpOperation(HttpRequest.HttpOp.POST);
+ case PUT: return request.setHttpOperation(HttpRequest.HttpOp.PUT);
+ case DELETE: return request.setHttpOperation(HttpRequest.HttpOp.DELETE);
+ default: throw new IllegalStateException("Unhandled method " + method);
+ }
+ }
+
+ private com.yahoo.container.jdisc.HttpResponse copyResponse(final HttpResult result) {
+ return new com.yahoo.container.jdisc.HttpResponse(result.getHttpReturnCode()) {
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ outputStream.write(Utf8.toBytes(result.getContent().toString()));
+ }
+
+ @Override
+ public void complete(){
+ copyResponseHeaders(result, getJdiscResponse());
+ }
+ };
+ }
+
+ private void copyPostData(com.yahoo.container.jdisc.HttpRequest request, HttpRequest legacyRequest) {
+ try {
+ legacyRequest.setPostContent(IOUtils.toString(request.getData(), "UTF-8"));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static void copyParameters(HttpRequest legacyRequest, com.yahoo.jdisc.http.HttpRequest jDiscRequest) {
+ for (String key : jDiscRequest.parameters().keySet()) {
+ for (String value : jDiscRequest.parameters().get(key)) {
+ legacyRequest.addUrlOption(key, value);
+ }
+ }
+ }
+
+ private static void copyRequestHeaders(HttpRequest legacyRequest, com.yahoo.jdisc.http.HttpRequest jDiscRequest) {
+ for (String key : jDiscRequest.headers().keySet()) {
+ for (String value : jDiscRequest.headers().get(key)) {
+ legacyRequest.addHttpHeader(key, value);
+ }
+ }
+ }
+
+ private static HeaderFields copyResponseHeaders(HttpResult result, Response response) {
+ HeaderFields headers = new HeaderFields();
+ for (HttpRequest.KeyValuePair keyValuePair : result.getHeaders()) {
+ response.headers().put((keyValuePair.getKey()), keyValuePair.getValue());
+ }
+ return headers;
+ }
+}
diff --git a/clustercontroller-apputil/src/main/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/JDiscMetricWrapper.java b/clustercontroller-apputil/src/main/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/JDiscMetricWrapper.java
new file mode 100644
index 00000000000..f905900e35f
--- /dev/null
+++ b/clustercontroller-apputil/src/main/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/JDiscMetricWrapper.java
@@ -0,0 +1,50 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.clustercontroller.apputil.communication.http;
+
+import com.yahoo.jdisc.Metric;
+import com.yahoo.vespa.clustercontroller.utils.util.MetricReporter;
+
+import java.util.logging.Logger;
+
+public class JDiscMetricWrapper implements MetricReporter {
+ private final Object lock = new Object();
+ private Metric m;
+
+ private static class ContextWrapper implements MetricReporter.Context {
+ Metric.Context wrappedContext;
+
+ public ContextWrapper(Metric.Context wrapped) {
+ this.wrappedContext = wrapped;
+ }
+ }
+
+ public JDiscMetricWrapper(Metric m) {
+ this.m = m;
+ }
+
+ public void updateMetricImplementation(Metric m) {
+ synchronized (lock) {
+ this.m = m;
+ }
+ }
+
+ public void set(String s, Number number, MetricReporter.Context context) {
+ synchronized (lock) {
+ ContextWrapper cw = (ContextWrapper) context;
+ m.set(s, number, cw == null ? null : cw.wrappedContext);
+ }
+ }
+
+ public void add(String s, Number number, MetricReporter.Context context) {
+ synchronized (lock) {
+ ContextWrapper cw = (ContextWrapper) context;
+ m.add(s, number, cw == null ? null : cw.wrappedContext);
+ }
+ }
+
+ public MetricReporter.Context createContext(java.util.Map<java.lang.String,?> stringMap) {
+ synchronized (lock) {
+ return new ContextWrapper(m.createContext(stringMap));
+ }
+ }
+}
diff --git a/clustercontroller-apputil/src/main/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/package-info.java b/clustercontroller-apputil/src/main/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/package-info.java
new file mode 100644
index 00000000000..78b66f37e9a
--- /dev/null
+++ b/clustercontroller-apputil/src/main/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.vespa.clustercontroller.apputil.communication.http;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/clustercontroller-apputil/src/test/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/ApacheAsyncHttpClientTest.java b/clustercontroller-apputil/src/test/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/ApacheAsyncHttpClientTest.java
new file mode 100644
index 00000000000..019122a1ee8
--- /dev/null
+++ b/clustercontroller-apputil/src/test/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/ApacheAsyncHttpClientTest.java
@@ -0,0 +1,239 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.clustercontroller.apputil.communication.http;
+
+import com.yahoo.vespa.clustercontroller.utils.communication.async.AsyncOperation;
+import com.yahoo.vespa.clustercontroller.utils.communication.async.AsyncUtils;
+import com.yahoo.vespa.clustercontroller.utils.communication.http.*;
+
+import java.util.LinkedList;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class ApacheAsyncHttpClientTest {
+
+ public static class Server {
+ public class Request {
+ HttpRequest request;
+ Object result;
+ String proxyHost;
+ int proxyPort;
+ long timeoutMs;
+
+ public Request(HttpRequest r, String proxyHost, int proxyPort, long timeoutMs) {
+ request = r;
+ this.proxyHost = proxyHost;
+ this.proxyPort = proxyPort;
+ this.timeoutMs = timeoutMs;
+ }
+
+ public void answer(Object result) {
+ synchronized (requests) {
+ this.result = result;
+ requests.notifyAll();
+ }
+ }
+ }
+ public final LinkedList<Request> requests = new LinkedList<>();
+
+ public Request createRequest(HttpRequest r, String proxyHost, int proxyPort, long timeoutMs) {
+ return new Request(r, proxyHost, proxyPort, timeoutMs);
+ }
+
+ public Request waitForRequest() {
+ synchronized (requests) {
+ while (true) {
+ if (!requests.isEmpty()) {
+ return requests.removeFirst();
+ }
+ try{ requests.wait(); } catch (InterruptedException e) {}
+ }
+ }
+ }
+ }
+
+ private Server server = new Server();
+ private int clientCount = 0;
+
+ public class Client implements SyncHttpClient {
+ String proxyHost;
+ int proxyPort;
+ long timeoutMs;
+ private boolean running = true;
+
+ public Client(String proxyHost, int proxyPort, long timeoutMs) {
+ this.proxyHost = proxyHost;
+ this.proxyPort = proxyPort;
+ this.timeoutMs = timeoutMs;
+ }
+
+ @Override
+ public HttpResult execute(HttpRequest r) {
+ synchronized (server.requests) {
+ Server.Request pair = server.createRequest(r, proxyHost, proxyPort, timeoutMs);
+ server.requests.addLast(pair);
+ server.requests.notifyAll();
+ while (running) {
+ try{ server.requests.wait(); } catch (InterruptedException e) {}
+ if (pair.result != null) {
+ if (pair.result instanceof HttpResult) {
+ return (HttpResult) pair.result;
+ } else {
+ throw new RuntimeException((Exception) pair.result);
+ }
+ } else {
+ }
+ }
+ }
+ return new HttpResult().setHttpCode(500, "Shutting down");
+ }
+
+ @Override
+ public void close() {
+ synchronized (server.requests) {
+ running = false;
+ server.requests.notifyAll();
+ }
+ }
+ }
+
+ public class ClientFactory implements ApacheAsyncHttpClient.SyncHttpClientFactory {
+ @Override
+ public SyncHttpClient createInstance(String proxyHost, int proxyPort, long timeoutMs) {
+ ++clientCount;
+ return new Client(proxyHost, proxyPort, timeoutMs);
+ }
+ }
+
+ private Executor executor;
+
+ @Before
+ public void setUp() {
+ executor = new ThreadPoolExecutor(10, 100, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(1000));
+ }
+
+ @Test
+ public void testOneInstancePerUniqueSettings() {
+ HttpResult result = new HttpResult().setHttpCode(201, "This worked out good");
+ ApacheAsyncHttpClient client = new ApacheAsyncHttpClient(executor, new ClientFactory());
+ ProxyAsyncHttpClient<HttpResult> proxyClient = new ProxyAsyncHttpClient<>(client, "proxy", 8080);
+
+ // A first request
+ HttpRequest req = new HttpRequest().setHost("www.yahoo.com").setPath("/foo").setTimeout(400);
+ AsyncOperation<HttpResult> op = proxyClient.execute(req);
+ Server.Request r = server.waitForRequest();
+ assertEquals(r.request.toString(true), "proxy", r.proxyHost);
+ assertEquals(r.request.toString(true), 8080, r.proxyPort);
+ assertEquals(r.request.toString(true), 80, r.request.getPort());
+ assertEquals(r.request.toString(true), "www.yahoo.com", r.request.getHost());
+ assertEquals(400, r.request.getTimeoutMillis());
+ r.answer(result);
+ AsyncUtils.waitFor(op);
+ assertTrue(op.isDone());
+ assertTrue(op.isSuccess());
+ assertEquals(result, op.getResult());
+ assertEquals(1, clientCount);
+
+ // A second request should reuse first instance
+ op = proxyClient.execute(req.clone());
+ r = server.waitForRequest();
+ r.answer(result);
+ AsyncUtils.waitFor(op);
+ assertEquals(1, clientCount);
+
+ // Altering timeout create a new one
+ op = proxyClient.execute(req.clone().setTimeout(800));
+ r = server.waitForRequest();
+ assertEquals(800, r.request.getTimeoutMillis());
+ r.answer(result);
+ AsyncUtils.waitFor(op);
+ assertEquals(2, clientCount);
+
+ // And altering proxy will create a new one
+ ProxyAsyncHttpClient<HttpResult> proxyClient2 = new ProxyAsyncHttpClient<>(client, "proxy2", 8080);
+ op = proxyClient2.execute(req.clone());
+ r = server.waitForRequest();
+ assertEquals(r.request.toString(true), "proxy2", r.proxyHost);
+ r.answer(result);
+ AsyncUtils.waitFor(op);
+ assertEquals(3, clientCount);
+
+ // And the old ones are still cached, even if port now is specified
+ op = proxyClient.execute(req.clone().setPort(80));
+ r = server.waitForRequest();
+ assertEquals(r.request.toString(true), "proxy", r.proxyHost);
+ assertEquals(r.request.toString(true), 8080, r.proxyPort);
+ assertEquals(r.request.toString(true), 80, r.request.getPort());
+ assertEquals(r.request.toString(true), "www.yahoo.com", r.request.getHost());
+ assertEquals(400, r.request.getTimeoutMillis());
+ r.answer(result);
+ AsyncUtils.waitFor(op);
+ assertEquals(3, clientCount);
+
+ client.close();
+ }
+
+ @Test
+ public void testFailingRequest() {
+ ApacheAsyncHttpClient client = new ApacheAsyncHttpClient(executor, new ClientFactory());
+ HttpRequest req = new HttpRequest();
+ AsyncOperation<HttpResult> op = client.execute(req);
+ Server.Request r = server.waitForRequest();
+ r.answer(new IllegalStateException("Failed to run"));
+ AsyncUtils.waitFor(op);
+ assertEquals(false, op.isSuccess());
+ assertTrue(op.getCause().getMessage(), op.getCause().getMessage().contains("Failed to run"));
+ }
+
+ @Test
+ public void testClose() {
+ ApacheAsyncHttpClient client = new ApacheAsyncHttpClient(executor, new ClientFactory());
+ HttpRequest req = new HttpRequest();
+ AsyncOperation<HttpResult> op = client.execute(req);
+ Server.Request r = server.waitForRequest();
+ client.close();
+ r.answer(new HttpResult());
+ AsyncUtils.waitFor(op);
+ assertEquals(true, op.isSuccess());
+
+ try{
+ client.execute(req);
+ assertTrue(false);
+ } catch (IllegalStateException e) {
+ assertTrue(e.getMessage(), e.getMessage().contains("Http client has been closed"));
+ }
+ }
+
+ @Test
+ public void testInvalidProxyRequest() {
+ ApacheAsyncHttpClient client = new ApacheAsyncHttpClient(executor, new ClientFactory());
+ HttpRequest req = new HttpRequest().setPath("foo");
+ try{
+ client.execute(req);
+ assertTrue(false);
+ } catch (IllegalStateException e) {
+ assertTrue(e.getMessage(), e.getMessage().contains("looks invalid"));
+ }
+ }
+
+ @Test
+ public void testNothingButGetCoverage() {
+ // There is never any hash conflict for equals to be false in actual use
+ HttpRequest r = new HttpRequest();
+ new ApacheAsyncHttpClient.Settings(r.clone().setTimeout(15))
+ .equals(new ApacheAsyncHttpClient.Settings(r));
+ // Only actual container is meant to use this constructor
+ ApacheAsyncHttpClient client = new ApacheAsyncHttpClient(executor);
+ client.execute(new HttpRequest());
+ client.close();
+ }
+
+}
diff --git a/clustercontroller-apputil/src/test/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/JDiscHttpRequestHandlerTest.java b/clustercontroller-apputil/src/test/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/JDiscHttpRequestHandlerTest.java
new file mode 100644
index 00000000000..1c5a0178414
--- /dev/null
+++ b/clustercontroller-apputil/src/test/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/JDiscHttpRequestHandlerTest.java
@@ -0,0 +1,47 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.clustercontroller.apputil.communication.http;
+
+import com.yahoo.vespa.clustercontroller.utils.communication.http.HttpRequest;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ * The handler is mostly tested through the apache tests, using it as endpoint here..
+ * This test class is just to test some special cases.
+ */
+public class JDiscHttpRequestHandlerTest {
+
+ private ThreadPoolExecutor executor;
+
+ @Before
+ public void setUp() {
+ executor = new ThreadPoolExecutor(10, 100, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000));
+ }
+
+ public void tearDown() {
+ executor.shutdown();
+ }
+
+ @Test
+ public void testInvalidMethod() throws Exception {
+ try{
+ HttpRequest request = new HttpRequest();
+ JDiscHttpRequestHandler.setOperation(request, com.yahoo.jdisc.http.HttpRequest.Method.CONNECT);
+ fail("Control should not reach here");
+ } catch (IllegalStateException e) {
+ assertEquals("Unhandled method CONNECT", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testNothingButAddCoverage() throws Exception {
+ new JDiscHttpRequestHandler.EmptyCompletionHandler().failed(null);
+ }
+}
diff --git a/clustercontroller-apputil/src/test/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/JDiscMetricWrapperTest.java b/clustercontroller-apputil/src/test/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/JDiscMetricWrapperTest.java
new file mode 100644
index 00000000000..95c99f5b94c
--- /dev/null
+++ b/clustercontroller-apputil/src/test/java/com/yahoo/vespa/clustercontroller/apputil/communication/http/JDiscMetricWrapperTest.java
@@ -0,0 +1,44 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.clustercontroller.apputil.communication.http;
+
+import com.yahoo.jdisc.Metric;
+import org.junit.Test;
+
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class JDiscMetricWrapperTest {
+ class MetricImpl implements Metric {
+ int calls = 0;
+ @Override
+ public void set(String s, Number number, Context context) { ++calls; }
+ @Override
+ public void add(String s, Number number, Context context) { ++calls; }
+ @Override
+ public Context createContext(Map<String, ?> stringMap) {
+ ++calls;
+ return new Context() {};
+ }
+ };
+
+ @Test
+ public void testSimple() {
+ MetricImpl impl1 = new MetricImpl();
+ MetricImpl impl2 = new MetricImpl();
+ JDiscMetricWrapper wrapper = new JDiscMetricWrapper(impl1);
+ wrapper.add("foo", 234, null);
+ wrapper.set("bar", 234, null);
+ assertTrue(wrapper.createContext(null) != null);
+ assertEquals(3, impl1.calls);
+ impl1.calls = 0;
+ wrapper.updateMetricImplementation(impl2);
+ wrapper.add("foo", 234, wrapper.createContext(null));
+ wrapper.set("bar", 234, wrapper.createContext(null));
+ assertTrue(wrapper.createContext(null) != null);
+ assertEquals(0, impl1.calls);
+ assertEquals(5, impl2.calls);
+
+ }
+}