aboutsummaryrefslogtreecommitdiffstats
path: root/clustercontroller-utils/src/main
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-utils/src/main
Publish
Diffstat (limited to 'clustercontroller-utils/src/main')
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/AsyncCallback.java7
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/AsyncOperation.java58
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/AsyncOperationImpl.java83
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/AsyncOperationListenImpl.java47
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/AsyncUtils.java11
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/PipedAsyncOperation.java23
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/RedirectedAsyncOperation.java63
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/SuccessfulAsyncCallback.java21
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/package-info.java5
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/AsyncHttpClient.java16
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/AsyncHttpClientWithBase.java37
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/HttpRequest.java148
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/HttpRequestHandler.java7
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/HttpResult.java87
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/JsonAsyncHttpClient.java57
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/JsonHttpResult.java74
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/LoggingAsyncHttpClient.java41
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/ProxyAsyncHttpClient.java28
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/RequestQueue.java85
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/SyncHttpClient.java9
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/TimeoutHandler.java149
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/package-info.java5
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/writer/HttpWriter.java63
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/StateRestAPI.java19
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/InternalFailure.java9
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/InvalidContentException.java11
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/InvalidOptionValueException.java11
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/MissingResourceException.java11
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/MissingUnitException.java21
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/NotMasterException.java9
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/OperationNotSupportedForUnitException.java19
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/OtherMasterException.java16
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/StateRestApiException.java32
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/UnknownMasterException.java9
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/package-info.java5
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/package-info.java5
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/requests/SetUnitStateRequest.java32
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/requests/UnitRequest.java6
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/requests/UnitStateRequest.java7
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/requests/package-info.java5
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/CurrentUnitState.java8
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/SetResponse.java28
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/SubUnitList.java10
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/UnitAttributes.java8
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/UnitMetrics.java8
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/UnitResponse.java11
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/UnitState.java7
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/package-info.java5
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/server/JsonReader.java96
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/server/JsonWriter.java103
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/server/RestApiHandler.java166
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/server/package-info.java5
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/test/FakeClock.java36
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/test/SettableClock.java9
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/test/TestTransport.java186
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/util/CertainlyCloneable.java22
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/util/Clock.java11
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/util/ComponentMetricReporter.java55
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/util/JSONObjectWrapper.java26
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/util/MetricReporter.java17
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/util/NoMetricReporter.java15
-rw-r--r--clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/util/package-info.java5
62 files changed, 2188 insertions, 0 deletions
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/AsyncCallback.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/AsyncCallback.java
new file mode 100644
index 00000000000..a68b90b6eef
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/AsyncCallback.java
@@ -0,0 +1,7 @@
+// 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.utils.communication.async;
+
+public interface AsyncCallback<T> {
+ /** Callback indicating the given operation has completed. */
+ public void done(AsyncOperation<T> op);
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/AsyncOperation.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/AsyncOperation.java
new file mode 100644
index 00000000000..3099f57ba09
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/AsyncOperation.java
@@ -0,0 +1,58 @@
+// 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.utils.communication.async;
+
+public interface AsyncOperation<T> {
+ /**
+ * Attempt to cancel the given operation.
+ * @return True if successfully cancelled. False if cancel is not supported, or operation already succeeded.
+ */
+ public boolean cancel();
+
+ /**
+ * Register a callback to be called when this operation completes. If operation is already completed, this callback
+ * will be called immediately upon registering. The same callback should not be registered multiple times. It is
+ * suggested to throw an exception if that should happen. Otherwise you may get one or more calls to that component.
+ */
+ void register(AsyncCallback<T> callback);
+
+ /**
+ * Remove a callback from the list to be called when operation is completed. If callback has not already been called
+ * at the time this function returns, it should never be called by this operation, unless re-registered.
+ */
+ void unregister(AsyncCallback<T> callback);
+
+ /**
+ * Get the name of the operation. Useful to identify what operation this is.
+ */
+ public String getName();
+
+ /**
+ * Get a description of the operation. May be empty. If operation is complex one might want to use a short name for
+ * simplicity, but have the whole request available if needed. In the HTTP case an application may for instance include
+ * the URL in the name, and add the request headers to the description.
+ */
+ public String getDescription();
+
+ /**
+ * Get the progress as a number between 0 and 1 where 0 means not started and 1 means operation is complete.
+ * A return value of null indicates that the operation is unable to track progress.
+ */
+ public Double getProgress();
+
+ /**
+ * Get the result of the operation.
+ * Note that some operations may not have a result if the operation failed.
+ */
+ public T getResult();
+
+ /** Get the cause of an operation failing. Returns null on successful operations. */
+ public Exception getCause();
+
+ /** Returns true if operation has been successfully cancelled. */
+ public boolean isCanceled();
+ /** Returns true if operation has completed. Regardless of whether it was a success or a failure. */
+ public boolean isDone();
+ /** Returns true if the operation was a success. */
+ public boolean isSuccess();
+
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/AsyncOperationImpl.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/AsyncOperationImpl.java
new file mode 100644
index 00000000000..79da91d40b2
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/AsyncOperationImpl.java
@@ -0,0 +1,83 @@
+// 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.utils.communication.async;
+
+import java.util.logging.Logger;
+
+public class AsyncOperationImpl<T> implements AsyncOperation<T> {
+ private static final Logger log = Logger.getLogger(AsyncOperationImpl.class.getName());
+ private final String name;
+ private final String description;
+ private boolean resultGotten = false; // Ensures result is set only once.
+ private boolean completed = false; // Indicates that operation is complete.
+ private boolean failed = false;
+ private T result;
+ private Exception failure;
+ private AsyncOperationListenImpl<T> listenImpl;
+
+ public AsyncOperationImpl(String name) {
+ this(name, null);
+ }
+ public AsyncOperationImpl(String name, String description) {
+ this.name = name;
+ this.description = description;
+ listenImpl = new AsyncOperationListenImpl<T>(this);
+ }
+
+ private boolean tagResultHandled() {
+ synchronized (listenImpl) {
+ if (resultGotten) {
+ log.fine("Operation " + this + " got result attempted set twice. This may occasionally happen if multiple "
+ + "sources are set to possibly terminate operations, such as for example if there is a separate cancel or timeout "
+ + "handler.");
+ return false;
+ }
+ resultGotten = true;
+ return true;
+ }
+ }
+
+ public void setFailure(Exception e) { setFailure(e, null); }
+ public void setFailure(Exception e, T partialResult) {
+ if (!tagResultHandled()) { return; }
+ failed = true;
+ failure = e;
+ this.result = partialResult;
+ completed = true;
+ listenImpl.notifyListeners();
+ }
+ public void setResult(T result) {
+ if (!tagResultHandled()) return;
+ this.result = result;
+ completed = true;
+ listenImpl.notifyListeners();
+ }
+
+ @Override
+ public String getName() { return name; }
+ @Override
+ public String getDescription() { return description; }
+ @Override
+ public String toString() { return "AsyncOperationImpl(" + name + ")"; }
+ @Override
+ public T getResult() { return result; }
+ @Override
+ public boolean cancel() { return false; }
+ @Override
+ public boolean isCanceled() { return false; }
+ @Override
+ public boolean isDone() { return completed; }
+ @Override
+ public boolean isSuccess() { return (completed && !failed); }
+ @Override
+ public Double getProgress() { return (completed ? 1.0 : null); }
+ @Override
+ public Exception getCause() { return failure; }
+ @Override
+ public void register(AsyncCallback<T> callback) {
+ listenImpl.register(callback);
+ }
+ @Override
+ public void unregister(AsyncCallback<T> callback) {
+ listenImpl.unregister(callback);
+ }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/AsyncOperationListenImpl.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/AsyncOperationListenImpl.java
new file mode 100644
index 00000000000..e9007f0c16e
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/AsyncOperationListenImpl.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.utils.communication.async;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.logging.Logger;
+
+public class AsyncOperationListenImpl<T> {
+ private static final Logger log = Logger.getLogger(AsyncOperationListenImpl.class.getName());
+ private final Collection<AsyncCallback<T>> listeners = new HashSet<>();
+ private boolean listenersNotified = false;
+ private AsyncOperation<T> op;
+
+ protected AsyncOperationListenImpl(AsyncOperation<T> op) {
+ this.op = op;
+ }
+
+ public void register(AsyncCallback<T> callback) {
+ synchronized (listeners) {
+ listeners.add(callback);
+ if (listenersNotified) callback.done(op);
+ }
+ }
+ public void unregister(AsyncCallback<T> callback) {
+ synchronized (listeners) {
+ listeners.remove(callback);
+ }
+ }
+
+ public void notifyListeners() {
+ synchronized (listeners) {
+ if (listenersNotified) return;
+ for(AsyncCallback<T> callback : listeners) {
+ try{
+ callback.done(op);
+ } catch (RuntimeException e) {
+ StringWriter sw = new StringWriter();
+ e.printStackTrace(new PrintWriter(sw));
+ log.warning("Callback '" + callback + "' threw exception on notify. Should not happen:\n" + sw);
+ }
+ }
+ listenersNotified = true;
+ }
+ }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/AsyncUtils.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/AsyncUtils.java
new file mode 100644
index 00000000000..71d6f534a9a
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/AsyncUtils.java
@@ -0,0 +1,11 @@
+// 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.utils.communication.async;
+
+public class AsyncUtils {
+
+ public static void waitFor(AsyncOperation op) {
+ while (!op.isDone()) {
+ try{ Thread.sleep(1); } catch (InterruptedException e) {}
+ }
+ }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/PipedAsyncOperation.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/PipedAsyncOperation.java
new file mode 100644
index 00000000000..a7ce5bb61a2
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/PipedAsyncOperation.java
@@ -0,0 +1,23 @@
+// 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.utils.communication.async;
+
+public abstract class PipedAsyncOperation<S, T> extends RedirectedAsyncOperation<S, T> {
+ private T result;
+
+ public PipedAsyncOperation(AsyncOperation<S> source) {
+ super(source);
+ setOnCompleteTask(new AsyncCallback<S>() {
+ @Override
+ public void done(AsyncOperation<S> op) {
+ result = convertResult(op.getResult());
+ }
+ });
+ }
+
+ public abstract T convertResult(S result);
+
+ @Override
+ public T getResult() {
+ return result;
+ }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/RedirectedAsyncOperation.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/RedirectedAsyncOperation.java
new file mode 100644
index 00000000000..fdaf0dcbf2b
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/RedirectedAsyncOperation.java
@@ -0,0 +1,63 @@
+// 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.utils.communication.async;
+
+/**
+ * Utility class in order to wrap com.yahoo.vespa.clustercontroller.utils.communication.async operation callbacks. Useful when translating com.yahoo.vespa.clustercontroller.utils.communication.async operations returning JSON to com.yahoo.vespa.clustercontroller.utils.communication.async operations returning specific values.
+ */
+public abstract class RedirectedAsyncOperation<S, T> implements AsyncOperation<T> {
+ protected final AsyncOperation<S> source;
+ private final AsyncOperationListenImpl<T> listenImpl;
+ private AsyncCallback<S> beforeCallbackTask;
+
+ public RedirectedAsyncOperation(AsyncOperation<S> source) {
+ this.source = source;
+ this.listenImpl = new AsyncOperationListenImpl<>(this);
+ source.register(new AsyncCallback<S>() {
+ @Override
+ public void done(AsyncOperation<S> op) { notifyDone(); }
+ });
+ }
+
+ public RedirectedAsyncOperation<S, T> setOnCompleteTask(AsyncCallback<S> beforeTask) {
+ beforeCallbackTask = beforeTask;
+ return this;
+ }
+ private void notifyDone() {
+ if (beforeCallbackTask != null) beforeCallbackTask.done(source);
+ listenImpl.notifyListeners();
+ }
+
+ @Override
+ public String getName() { return source.getName(); }
+
+ @Override
+ public String getDescription() { return source.getDescription(); }
+
+ @Override
+ public boolean cancel() { return source.cancel(); }
+
+ @Override
+ public boolean isCanceled() { return source.isCanceled(); }
+
+ @Override
+ public boolean isDone() { return source.isDone(); }
+
+ @Override
+ public boolean isSuccess() { return source.isSuccess(); }
+
+ @Override
+ public Double getProgress() { return source.getProgress(); }
+
+ @Override
+ public Exception getCause() { return source.getCause(); }
+
+ @Override
+ public void register(AsyncCallback<T> callback) {
+ listenImpl.register(callback);
+ }
+
+ @Override
+ public void unregister(AsyncCallback<T> callback) {
+ listenImpl.unregister(callback);
+ }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/SuccessfulAsyncCallback.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/SuccessfulAsyncCallback.java
new file mode 100644
index 00000000000..0572465aac5
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/SuccessfulAsyncCallback.java
@@ -0,0 +1,21 @@
+// 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.utils.communication.async;
+
+public abstract class SuccessfulAsyncCallback<Source, Target> implements AsyncCallback<Source> {
+ private final AsyncOperationImpl<Target> target;
+
+ public SuccessfulAsyncCallback(final AsyncOperationImpl<Target> target) {
+ this.target = target;
+ }
+
+ public void done(AsyncOperation<Source> sourceOp) {
+ if (sourceOp.isSuccess()) {
+ successfullyDone(sourceOp);
+ } else {
+ target.setFailure(sourceOp.getCause());
+ }
+ }
+
+ public abstract void successfullyDone(AsyncOperation<Source> op);
+
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/package-info.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/package-info.java
new file mode 100644
index 00000000000..205da8cf98f
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/async/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.utils.communication.async;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/AsyncHttpClient.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/AsyncHttpClient.java
new file mode 100644
index 00000000000..5a5dd21c8f5
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/AsyncHttpClient.java
@@ -0,0 +1,16 @@
+// 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.utils.communication.http;
+
+import com.yahoo.vespa.clustercontroller.utils.communication.async.AsyncOperation;
+
+/**
+ * Abstraction of an asynchronious HTTP client, such that applications don't need to depend directly on an HTTP client.
+ */
+public interface AsyncHttpClient<V extends HttpResult> {
+
+ public AsyncOperation<V> execute(HttpRequest r);
+
+ /** Attempt to cancel all pending operations and shut down the client. */
+ public void close();
+
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/AsyncHttpClientWithBase.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/AsyncHttpClientWithBase.java
new file mode 100644
index 00000000000..419c19f529b
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/AsyncHttpClientWithBase.java
@@ -0,0 +1,37 @@
+// 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.utils.communication.http;
+
+import com.yahoo.vespa.clustercontroller.utils.communication.async.AsyncOperation;
+
+public class AsyncHttpClientWithBase<V extends HttpResult> implements AsyncHttpClient<V> {
+ protected final AsyncHttpClient<V> client;
+ private HttpRequest baseRequest = new HttpRequest();
+
+ public AsyncHttpClientWithBase(AsyncHttpClient<V> client) {
+ if (client == null) throw new IllegalArgumentException("HTTP client must be set.");
+ this.client = client;
+ }
+
+ /**
+ * If all your http requests have common features you want to set once, you can provide those values in a base
+ * request. For instance, if you specify a host and a port using this function, all your requests will use that
+ * host and port unless specified in the request you execute.
+ */
+ public void setHttpRequestBase(HttpRequest r) {
+ this.baseRequest = (r == null ? new HttpRequest() : r.clone());
+ }
+
+ public HttpRequest getHttpRequestBase() {
+ return baseRequest;
+ }
+
+ @Override
+ public AsyncOperation<V> execute(HttpRequest r) {
+ return client.execute(baseRequest.merge(r));
+ }
+
+ @Override
+ public void close() {
+ client.close();
+ }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/HttpRequest.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/HttpRequest.java
new file mode 100644
index 00000000000..c64da81cac1
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/HttpRequest.java
@@ -0,0 +1,148 @@
+// 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.utils.communication.http;
+
+import com.yahoo.vespa.clustercontroller.utils.util.CertainlyCloneable;
+
+import java.util.LinkedList;
+import java.util.List;
+
+public class HttpRequest extends CertainlyCloneable<HttpRequest> {
+ public static class KeyValuePair {
+ public String key;
+ public String value;
+
+ public KeyValuePair(String k, String v) { this.key = k; this.value = v; }
+
+ public String getValue() {
+ return value;
+ }
+
+ public String getKey() {
+ return key;
+ }
+ }
+ public enum HttpOp { GET, POST, PUT, DELETE }
+
+ private String host;
+ private int port;
+ private String path;
+ private List<KeyValuePair> urlOptions = new LinkedList<>();
+ private List<KeyValuePair> headers = new LinkedList<>();
+ private long timeoutMillis;
+ private Object postContent;
+ private HttpOp httpOperation;
+
+ public HttpRequest() {}
+
+ public String getHost() { return host; }
+ public int getPort() { return port; }
+ public String getPath() { return path; }
+ public List<KeyValuePair> getUrlOptions() { return urlOptions; }
+ public String getOption(String key, String defaultValue) {
+ for (KeyValuePair value : urlOptions) {
+ if (value.key.equals(key)) return value.value;
+ }
+ return defaultValue;
+
+ }
+ public List<KeyValuePair> getHeaders() { return headers; }
+ public String getHeader(String key, String defaultValue) {
+ for (KeyValuePair header : headers) {
+ if (header.key.equals(key)) return header.value;
+ }
+ return defaultValue;
+ }
+ public long getTimeoutMillis() { return timeoutMillis; }
+ public Object getPostContent() { return postContent; }
+ public HttpOp getHttpOperation() { return httpOperation; }
+
+ public HttpRequest setHost(String hostname) { this.host = hostname; return this; }
+ public HttpRequest setPort(int port) { this.port = port; return this; }
+ public HttpRequest setPath(String path) {
+ this.path = path; return this;
+ }
+ public HttpRequest addUrlOption(String key, String value) { this.urlOptions.add(new KeyValuePair(key, value)); return this; }
+ public HttpRequest setUrlOptions(List<KeyValuePair> options) { this.urlOptions.clear(); this.urlOptions.addAll(options); return this; }
+ public HttpRequest addHttpHeader(String key, String value) { this.headers.add(new KeyValuePair(key, value)); return this; }
+ public HttpRequest setTimeout(long timeoutMillis) { this.timeoutMillis = timeoutMillis; return this; }
+ public HttpRequest setPostContent(Object content) { this.postContent = content; return this; }
+ public HttpRequest setHttpOperation(HttpOp op) { this.httpOperation = op; return this; }
+
+ /** Create a copy of this request, and override what is specified in the input in the new request. */
+ public HttpRequest merge(HttpRequest r) {
+ HttpRequest copy = clone();
+ if (r.host != null) copy.host = r.host;
+ if (r.port != 0) copy.port = r.port;
+ if (r.path != null) copy.path = r.path;
+ for (KeyValuePair h : r.headers) {
+ boolean containsElement = false;
+ for (KeyValuePair h2 : copy.headers) { containsElement |= (h.key.equals(h2.key)); }
+ if (!containsElement) copy.headers.add(h);
+ }
+ for (KeyValuePair h : r.urlOptions) {
+ boolean containsElement = false;
+ for (KeyValuePair h2 : copy.urlOptions) { containsElement |= (h.key.equals(h2.key)); }
+ if (!containsElement) copy.urlOptions.add(h);
+ }
+ if (r.timeoutMillis != 0) copy.timeoutMillis = r.timeoutMillis;
+ if (r.postContent != null) copy.postContent = r.postContent;
+ if (r.httpOperation != null) copy.httpOperation = r.httpOperation;
+ return copy;
+ }
+
+ @Override
+ public HttpRequest clone() {
+ HttpRequest r = (HttpRequest) super.clone();
+ r.headers = new LinkedList<>(r.headers);
+ r.urlOptions = new LinkedList<>(r.urlOptions);
+ return r;
+ }
+
+ @Override
+ public String toString() { return toString(false); }
+ public String toString(boolean verbose) {
+ String httpOp = (httpOperation != null ? httpOperation.toString()
+ : (postContent == null ? "GET?" : "POST?"));
+ StringBuilder sb = new StringBuilder().append(httpOp).append(" http:");
+ if (host != null) {
+ sb.append("//").append(host);
+ if (port != 0) sb.append(':').append(port);
+ }
+ if (path == null || path.isEmpty()) {
+ sb.append('/');
+ } else {
+ if (path.charAt(0) != '/') sb.append('/');
+ sb.append(path);
+ }
+ if (urlOptions != null && urlOptions.size() > 0) {
+ boolean first = (path == null || path.indexOf('?') < 0);
+ for (KeyValuePair e : urlOptions) {
+ sb.append(first ? '?' : '&');
+ first = false;
+ sb.append(e.key).append('=').append(e.value);
+ }
+ }
+ if (verbose) {
+ for (KeyValuePair p : headers) {
+ sb.append('\n').append(p.key).append(": ").append(p.value);
+ }
+ if (postContent != null && !postContent.toString().isEmpty()) {
+ sb.append("\n\n").append(postContent.toString());
+ }
+ }
+ return sb.toString();
+ }
+
+ public void verifyComplete() {
+ if (path == null) throw new IllegalStateException("HTTP requests must have a path set. Use '/' for top level");
+ if (httpOperation == null) throw new IllegalStateException("HTTP requests must have an HTTP method defined");
+ }
+
+ public boolean equals(Object o) {
+ // Equals is only used for debugging as far as we know. Refer to verbose toString to simplify
+ if (o instanceof HttpRequest) {
+ return toString(true).equals(((HttpRequest) o).toString(true));
+ }
+ return false;
+ }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/HttpRequestHandler.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/HttpRequestHandler.java
new file mode 100644
index 00000000000..b8acfa33e71
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/HttpRequestHandler.java
@@ -0,0 +1,7 @@
+// 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.utils.communication.http;
+
+public interface HttpRequestHandler {
+
+ public HttpResult handleRequest(HttpRequest request) throws Exception;
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/HttpResult.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/HttpResult.java
new file mode 100644
index 00000000000..7e2bf90b92c
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/HttpResult.java
@@ -0,0 +1,87 @@
+// 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.utils.communication.http;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+
+public class HttpResult {
+ private static class HttpReturnCode {
+ private final int code;
+ private final String message;
+
+ public HttpReturnCode(int code, String message) {
+ this.code = code;
+ this.message = message;
+ }
+ public boolean isSuccess() { return (code >= 200 && code < 300); }
+ public int getCode() { return code; }
+ public String getMessage() { return message; }
+ }
+ private HttpReturnCode httpReturnCode;
+ private final List<HttpRequest.KeyValuePair> headers;
+ private Object content;
+
+ public HttpResult() {
+ httpReturnCode = new HttpReturnCode(200, "OK");
+ headers = new LinkedList<>();
+ }
+
+ public HttpResult(HttpResult other) {
+ httpReturnCode = other.httpReturnCode;
+ headers = other.headers;
+ content = other.content;
+ }
+
+ public HttpResult setHttpCode(int code, String description) {
+ this.httpReturnCode = new HttpReturnCode(code, description);
+ return this;
+ }
+
+ public HttpResult setContent(Object content) {
+ this.content = content;
+ return this;
+ }
+
+ public HttpResult addHeader(String key, String value) {
+ headers.add(new HttpRequest.KeyValuePair(key, value));
+ return this;
+ }
+
+ public boolean isSuccess() { return httpReturnCode.isSuccess(); }
+ public int getHttpReturnCode() { return httpReturnCode.getCode(); }
+ public String getHttpReturnCodeDescription() { return httpReturnCode.getMessage(); }
+ public Collection<HttpRequest.KeyValuePair> getHeaders() { return headers; }
+ public String getHeader(String key) {
+ for (HttpRequest.KeyValuePair p : headers) {
+ if (p.getKey().equals(key)) return p.getValue();
+ }
+ return null;
+ }
+
+ public Object getContent() { return content; }
+
+ @Override
+ public String toString() { return toString(false); }
+ public String toString(boolean verbose) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("HTTP ").append(httpReturnCode.getCode()).append('/').append(httpReturnCode.getMessage());
+ if (verbose) {
+ for(HttpRequest.KeyValuePair header : headers) {
+ sb.append('\n').append(header.getKey()).append(": ").append(header.getValue());
+ }
+ if (content != null) {
+ StringBuilder contentBuilder = new StringBuilder();
+ printContent(contentBuilder);
+ String s = contentBuilder.toString();
+ if (!s.isEmpty()) {
+ sb.append("\n\n").append(s);
+ }
+ }
+ }
+ return sb.toString();
+ }
+ public void printContent(StringBuilder sb) {
+ sb.append(content.toString());
+ }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/JsonAsyncHttpClient.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/JsonAsyncHttpClient.java
new file mode 100644
index 00000000000..bffac7f68e3
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/JsonAsyncHttpClient.java
@@ -0,0 +1,57 @@
+// 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.utils.communication.http;
+
+import com.yahoo.vespa.clustercontroller.utils.communication.async.AsyncOperation;
+import com.yahoo.vespa.clustercontroller.utils.communication.async.RedirectedAsyncOperation;
+import org.codehaus.jettison.json.JSONException;
+import org.codehaus.jettison.json.JSONObject;
+
+/**
+ * Wrapped for the HTTP client, converting requests to/from JSON.
+ */
+public class JsonAsyncHttpClient implements AsyncHttpClient<JsonHttpResult> {
+ private AsyncHttpClient<HttpResult> client;
+ private boolean verifyRequestContentAsJson = true;
+ private boolean addJsonContentType = true;
+
+ public JsonAsyncHttpClient(AsyncHttpClient<HttpResult> client) {
+ this.client = client;
+ }
+
+ public JsonAsyncHttpClient verifyRequestContentAsJson(boolean doIt) {
+ verifyRequestContentAsJson = doIt;
+ return this;
+ }
+
+ public JsonAsyncHttpClient addJsonContentType(boolean doIt) {
+ addJsonContentType = doIt;
+ return this;
+ }
+
+ public AsyncOperation<JsonHttpResult> execute(HttpRequest r) {
+ if (verifyRequestContentAsJson) {
+ if (r.getPostContent() != null && !(r.getPostContent() instanceof JSONObject)) {
+ try{
+ r = r.clone().setPostContent(new JSONObject(r.getPostContent().toString()));
+ } catch (JSONException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+ }
+ if (addJsonContentType && r.getPostContent() != null) {
+ r = r.clone().addHttpHeader("Content-Type", "application/json");
+ }
+ final AsyncOperation<HttpResult> op = client.execute(r);
+ return new RedirectedAsyncOperation<HttpResult, JsonHttpResult>(op) {
+ @Override
+ public JsonHttpResult getResult() {
+ return (op.getResult() == null ? null : new JsonHttpResult(op.getResult()));
+ }
+ };
+ }
+
+ @Override
+ public void close() {
+ client.close();
+ }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/JsonHttpResult.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/JsonHttpResult.java
new file mode 100644
index 00000000000..e26ea4fc3a2
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/JsonHttpResult.java
@@ -0,0 +1,74 @@
+// 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.utils.communication.http;
+
+import com.yahoo.vespa.clustercontroller.utils.util.JSONObjectWrapper;
+import org.codehaus.jettison.json.JSONException;
+import org.codehaus.jettison.json.JSONObject;
+
+public class JsonHttpResult extends HttpResult {
+ private JSONObject json;
+ private boolean failedParsing = false;
+
+
+ public JsonHttpResult() {
+ addHeader("Content-Type", "application/json");
+ }
+
+ public JsonHttpResult(HttpResult other) {
+ super(other);
+
+ if (other.getContent() == null) {
+ setParsedJson(new JSONObject());
+ return;
+ }
+ try{
+ if (other.getContent() instanceof JSONObject) {
+ setParsedJson((JSONObject) other.getContent());
+ } else {
+ setParsedJson(new JSONObject(other.getContent().toString()));
+ }
+ } catch (JSONException e) {
+ failedParsing = true;
+ setParsedJson(createErrorJson(e.getMessage(), other));
+ }
+ }
+
+ private JSONObject createErrorJson(String error, HttpResult other) {
+ return new JSONObjectWrapper()
+ .put("error", "Invalid JSON in output: " + error)
+ .put("output", other.getContent().toString());
+ }
+
+ public JsonHttpResult setJson(JSONObject o) {
+ setContent(o);
+ json = o;
+ return this;
+ }
+
+ private void setParsedJson(JSONObject o) {
+ json = o;
+ }
+
+ public JSONObject getJson() {
+ return json;
+ }
+
+ @Override
+ public void printContent(StringBuilder sb) {
+ if (failedParsing) {
+ super.printContent(sb);
+ return;
+ }
+ if (json != null) {
+ sb.append("JSON: ");
+ try{
+ sb.append(json.toString(2));
+ } catch (JSONException e) {
+ sb.append(json.toString());
+ }
+ } else {
+ super.printContent(sb);
+ }
+ }
+
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/LoggingAsyncHttpClient.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/LoggingAsyncHttpClient.java
new file mode 100644
index 00000000000..d4aff12146d
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/LoggingAsyncHttpClient.java
@@ -0,0 +1,41 @@
+// 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.utils.communication.http;
+
+import com.yahoo.vespa.clustercontroller.utils.communication.async.AsyncOperation;
+import com.yahoo.vespa.clustercontroller.utils.communication.async.PipedAsyncOperation;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class LoggingAsyncHttpClient<T extends HttpResult> extends AsyncHttpClientWithBase<T> {
+ private static final Logger log = Logger.getLogger(LoggingAsyncHttpClient.class.getName());
+ private int requestCounter = 0;
+
+ public LoggingAsyncHttpClient(AsyncHttpClient<T> client) {
+ super(client);
+ log.info("Logging HTTP requests if fine logging level is added");
+ }
+
+ public AsyncOperation<T> execute(HttpRequest r) {
+ final int requestCount = ++requestCounter;
+ log.fine("Issuing HTTP request " + requestCount + ": " + r.toString(true));
+ final AsyncOperation<T> op = client.execute(r);
+ return new PipedAsyncOperation<T, T>(op) {
+ @Override
+ public T convertResult(T result) {
+ if (log.isLoggable(Level.FINE)) {
+ if (op.isSuccess()) {
+ log.fine("HTTP request " + requestCount + " completed: " + result.toString(true));
+ } else {
+ StringWriter sw = new StringWriter();
+ op.getCause().printStackTrace(new PrintWriter(sw));
+ log.fine("HTTP request " + requestCount + " failed: " + sw);
+ }
+ }
+ return result;
+ }
+ };
+ }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/ProxyAsyncHttpClient.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/ProxyAsyncHttpClient.java
new file mode 100644
index 00000000000..6b63027d0af
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/ProxyAsyncHttpClient.java
@@ -0,0 +1,28 @@
+// 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.utils.communication.http;
+
+import com.yahoo.vespa.clustercontroller.utils.communication.async.AsyncOperation;
+
+public class ProxyAsyncHttpClient<V extends HttpResult> extends AsyncHttpClientWithBase<V> {
+ private final String proxyHost;
+ private final int proxyPort;
+
+ public ProxyAsyncHttpClient(AsyncHttpClient<V> client, String proxyHost, int proxyPort) {
+ super(client);
+ this.proxyHost = proxyHost;
+ this.proxyPort = proxyPort;
+ }
+
+ @Override
+ public AsyncOperation<V> execute(HttpRequest r) {
+ r = getHttpRequestBase().merge(r);
+ if (r.getHost() == null || r.getPath() == null) {
+ throw new IllegalStateException("Host and path must be set prior to being able to proxy an HTTP request");
+ }
+ StringBuilder path = new StringBuilder().append(r.getHost());
+ if (r.getPort() != 0) path.append(':').append(r.getPort());
+ if (r.getPath().isEmpty() || r.getPath().charAt(0) != '/') path.append('/');
+ path.append(r.getPath());
+ return client.execute(r.setHost(proxyHost).setPort(proxyPort).setPath(path.toString()));
+ }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/RequestQueue.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/RequestQueue.java
new file mode 100644
index 00000000000..6388d32f98d
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/RequestQueue.java
@@ -0,0 +1,85 @@
+// 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.utils.communication.http;
+
+import com.yahoo.vespa.clustercontroller.utils.communication.async.AsyncCallback;
+import com.yahoo.vespa.clustercontroller.utils.communication.async.AsyncOperation;
+
+import java.util.LinkedList;
+import java.util.logging.Logger;
+
+/**
+ * Utility class to schedule HTTP requests and keeping a maximum amount of them pending at a time.
+ */
+public class RequestQueue<V extends HttpResult> {
+ private static final Logger log = Logger.getLogger(RequestQueue.class.getName());
+ private final AsyncHttpClient<V> httpClient;
+ private final LinkedList<Request<V>> requestQueue = new LinkedList<>();
+ private final int maxPendingRequests;
+ private int pendingRequests = 0;
+
+ public RequestQueue(AsyncHttpClient<V> httpClient, int maxPendingRequests) {
+ this.httpClient = httpClient;
+ this.maxPendingRequests = maxPendingRequests;
+ }
+
+ public boolean empty() {
+ synchronized (requestQueue) {
+ return (requestQueue.isEmpty() && pendingRequests == 0);
+ }
+ }
+
+ public void waitUntilEmpty() throws InterruptedException {
+ synchronized (requestQueue) {
+ while (!empty()) {
+ requestQueue.wait();
+ }
+ }
+ }
+
+ public void schedule(HttpRequest request, AsyncCallback<V> callback) {
+ log.fine("Scheduling " + request + " call");
+ synchronized (requestQueue) {
+ requestQueue.addLast(new Request<>(request, callback));
+ sendMore();
+ }
+ }
+
+ private void sendMore() {
+ while (pendingRequests < maxPendingRequests && !requestQueue.isEmpty()) {
+ Request<V> call = requestQueue.removeFirst();
+ log.fine("Sending " + call.getRequest() + ".");
+ ++pendingRequests;
+ AsyncOperation<V> op = httpClient.execute(call.getRequest());
+ op.register(call);
+ }
+ }
+
+ private class Request<V extends HttpResult> implements AsyncCallback<V> {
+ private final HttpRequest request;
+ private final AsyncCallback<V> callback;
+
+ Request(HttpRequest request, AsyncCallback<V> callback) {
+ this.request = request;
+ this.callback = callback;
+ }
+
+ public HttpRequest getRequest() { return request; }
+
+ @Override
+ public void done(AsyncOperation<V> op) {
+ if (op.isSuccess()) {
+ log.fine("Operation " + op.getName() + " completed successfully");
+ } else {
+ log.fine("Operation " + op.getName() + " failed: " + op.getCause());
+ }
+ synchronized (requestQueue) {
+ --pendingRequests;
+ }
+ callback.done(op);
+ synchronized (requestQueue) {
+ requestQueue.notifyAll();
+ sendMore();
+ }
+ }
+ }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/SyncHttpClient.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/SyncHttpClient.java
new file mode 100644
index 00000000000..cdb20be3338
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/SyncHttpClient.java
@@ -0,0 +1,9 @@
+// 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.utils.communication.http;
+
+public interface SyncHttpClient {
+ public HttpResult execute(HttpRequest r);
+
+ /** Attempt to cancel all pending operations and shut down the client. */
+ public void close();
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/TimeoutHandler.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/TimeoutHandler.java
new file mode 100644
index 00000000000..c406dafe063
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/TimeoutHandler.java
@@ -0,0 +1,149 @@
+// 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.utils.communication.http;
+
+import com.yahoo.vespa.clustercontroller.utils.communication.async.AsyncCallback;
+import com.yahoo.vespa.clustercontroller.utils.communication.async.AsyncOperation;
+import com.yahoo.vespa.clustercontroller.utils.communication.async.AsyncOperationImpl;
+import com.yahoo.vespa.clustercontroller.utils.util.Clock;
+
+import java.util.*;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Logger;
+
+public class TimeoutHandler<V extends HttpResult> extends AsyncHttpClientWithBase<V> {
+ public static class InternalRequest<V extends HttpResult> extends AsyncOperationImpl<V> {
+ final AsyncOperation<V> operation;
+ long startTime;
+ long timeout;
+
+ public InternalRequest(AsyncOperation<V> op, long startTime, long timeout) {
+ super(op.getName(), op.getDescription());
+ this.operation = op;
+ this.startTime = startTime;
+ this.timeout = timeout;
+ op.register(new AsyncCallback<V>() {
+ @Override
+ public void done(AsyncOperation<V> op) {
+ if (!isDone()) {
+ if (op.isSuccess()) {
+ setResult(op.getResult());
+ } else {
+ setFailure(op.getCause(), op.getResult());
+ }
+ }
+ }
+ });
+ }
+
+ public long getTimeoutTime() { return startTime + timeout; }
+
+ public void handleTimeout(long currentTime) {
+ long timePassed = currentTime - startTime;
+ this.setFailure(new TimeoutException("Operation timeout. " + timePassed + " ms since operation was issued. Timeout was " + timeout + " ms."));
+ operation.cancel();
+ }
+
+ @Override
+ public boolean cancel() { return operation.cancel(); }
+ @Override
+ public boolean isCanceled() { return operation.isCanceled(); }
+ @Override
+ public Double getProgress() { return (isDone() ? Double.valueOf(1.0) : operation.getProgress()); }
+ }
+
+ public static class ChangeLogger {
+ private InternalRequest lastTimeoutLogged = null;
+ private boolean emptyLogged = true;
+
+ public void logChanges(TreeMap<Long, InternalRequest> requests) {
+ if (requests.isEmpty()) {
+ if (!emptyLogged) {
+ log.finest("No more pending requests currently.");
+ emptyLogged = true;
+ }
+ } else {
+ emptyLogged = false;
+ InternalRequest r = requests.firstEntry().getValue();
+ if (lastTimeoutLogged == null || !lastTimeoutLogged.equals(r)) {
+ lastTimeoutLogged = r;
+ log.finest("Next operation to possibly timeout will do so at " + r.getTimeoutTime());
+ }
+ }
+ }
+ }
+
+ private final static Logger log = Logger.getLogger(TimeoutHandler.class.getName());
+ private final TreeMap<Long, InternalRequest> requests = new TreeMap<>();
+ private final ChangeLogger changeLogger = new ChangeLogger();
+ private final Clock clock;
+ private boolean run = true;
+ private Runnable timeoutHandler = new Runnable() {
+ @Override
+ public void run() {
+ log.fine("Starting timeout monitor thread");
+ while (true) {
+ performTimeoutHandlerTick();
+ synchronized (clock) {
+ try{ clock.wait(100); } catch (InterruptedException e) {}
+ if (!run) break;
+ }
+ }
+ log.fine("Stopped timeout monitor thread");
+ }
+ };
+
+ public TimeoutHandler(Executor executor, Clock clock, AsyncHttpClient<V> client) {
+ super(client);
+ this.clock = clock;
+ executor.execute(timeoutHandler);
+ }
+
+ @Override
+ public void close() {
+ synchronized (clock) {
+ run = false;
+ clock.notifyAll();
+ }
+ synchronized (requests) {
+ for (InternalRequest r : requests.values()) {
+ r.operation.cancel();
+ r.setFailure(new TimeoutException("Timeout handler shutting down. Shutting down all requests monitored."));
+ }
+ requests.clear();
+ }
+ }
+
+ @Override
+ public AsyncOperation<V> execute(HttpRequest r) {
+ AsyncOperation<V> op = super.execute(r);
+ InternalRequest<V> request = new InternalRequest<>(op, clock.getTimeInMillis(), r.getTimeoutMillis());
+ synchronized (requests) {
+ requests.put(request.getTimeoutTime(), request);
+ }
+ return request;
+ }
+
+ void performTimeoutHandlerTick() {
+ synchronized (requests) {
+ removeCompletedRequestsFromTimeoutList();
+ handleTimeoutsAtTime(clock.getTimeInMillis());
+ changeLogger.logChanges(requests);
+ }
+ }
+
+ private void removeCompletedRequestsFromTimeoutList() {
+ while (!requests.isEmpty() && requests.firstEntry().getValue().operation.isDone()) {
+ requests.remove(requests.firstEntry().getKey());
+ log.finest("Removed completed request from operation timeout list.");
+ }
+ }
+
+ private void handleTimeoutsAtTime(long currentTime) {
+ Map<Long, InternalRequest> timeouts = requests.subMap(0l, currentTime + 1);
+ for (InternalRequest r : timeouts.values()) {
+ r.handleTimeout(currentTime);
+ requests.values().remove(r);
+ }
+ }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/package-info.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/package-info.java
new file mode 100644
index 00000000000..72155601ba5
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/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.utils.communication.http;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/writer/HttpWriter.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/writer/HttpWriter.java
new file mode 100644
index 00000000000..ab3816d10f3
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/communication/http/writer/HttpWriter.java
@@ -0,0 +1,63 @@
+// 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.utils.communication.http.writer;
+
+public class HttpWriter {
+ private final StringBuilder builder = new StringBuilder();
+
+ private String title = "Untitled page";
+ enum State { HEADER, BODY, FINALIZED };
+ private State state = State.HEADER;
+
+ public HttpWriter() {
+ }
+
+ public HttpWriter addTitle(String title) {
+ verifyState(State.HEADER);
+ this.title = title;
+ return this;
+ }
+
+ public HttpWriter write(String paragraph) {
+ verifyState(State.BODY);
+ builder.append(" <p>\n")
+ .append(" " + paragraph + "\n")
+ .append(" </p>\n");
+ return this;
+ }
+
+ public HttpWriter writeLink(String name, String link) {
+ verifyState(State.BODY);
+ builder.append(" <a href=\"" + link + "\">" + name + "</a>\n");
+ return this;
+ }
+
+ private void verifyState(State state) {
+ if (this.state == state) return;
+ if (state != State.FINALIZED && this.state == State.FINALIZED) {
+ throw new IllegalStateException("HTTP page already finalized");
+ }
+ if (state == State.HEADER && this.state == State.BODY) {
+ throw new IllegalStateException("Have already started to write body. Cannot alter header");
+ }
+ if (this.state == State.HEADER) {
+ builder.append("<html>\n"
+ + " <head>\n"
+ + " <title>" + title + "</title>\n"
+ + " </head>\n"
+ + " <body>\n"
+ + " <h1>" + title + "</h1>\n");
+ this.state = State.BODY;
+ if (this.state == state) return;
+ }
+ // If we get here we are in state body and want to get finalized
+ builder.append(" </body>\n"
+ + "</html>\n");
+ this.state = State.FINALIZED;
+ }
+
+ public String toString() {
+ verifyState(State.FINALIZED);
+ return builder.toString();
+ }
+
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/StateRestAPI.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/StateRestAPI.java
new file mode 100644
index 00000000000..b380130d252
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/StateRestAPI.java
@@ -0,0 +1,19 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * Interface to implement for backends that want to have a State Rest API.
+ */
+package com.yahoo.vespa.clustercontroller.utils.staterestapi;
+
+import com.yahoo.vespa.clustercontroller.utils.staterestapi.errors.StateRestApiException;
+import com.yahoo.vespa.clustercontroller.utils.staterestapi.requests.SetUnitStateRequest;
+import com.yahoo.vespa.clustercontroller.utils.staterestapi.requests.UnitStateRequest;
+import com.yahoo.vespa.clustercontroller.utils.staterestapi.response.SetResponse;
+import com.yahoo.vespa.clustercontroller.utils.staterestapi.response.UnitResponse;
+
+public interface StateRestAPI {
+
+ UnitResponse getState(UnitStateRequest request) throws StateRestApiException;
+
+ SetResponse setUnitState(SetUnitStateRequest request) throws StateRestApiException;
+
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/InternalFailure.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/InternalFailure.java
new file mode 100644
index 00000000000..10b629a29e4
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/InternalFailure.java
@@ -0,0 +1,9 @@
+// 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.utils.staterestapi.errors;
+
+public class InternalFailure extends StateRestApiException {
+
+ public InternalFailure(String description) {
+ super("Internal failure. Should not happen: " + description);
+ }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/InvalidContentException.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/InvalidContentException.java
new file mode 100644
index 00000000000..86f6dc81926
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/InvalidContentException.java
@@ -0,0 +1,11 @@
+// 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.utils.staterestapi.errors;
+
+public class InvalidContentException extends StateRestApiException {
+
+ public InvalidContentException(String description) {
+ super(description);
+ setHtmlCode(400);
+ setHtmlStatus("Content of HTTP request had invalid data");
+ }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/InvalidOptionValueException.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/InvalidOptionValueException.java
new file mode 100644
index 00000000000..48700e3f343
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/InvalidOptionValueException.java
@@ -0,0 +1,11 @@
+// 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.utils.staterestapi.errors;
+
+public class InvalidOptionValueException extends StateRestApiException {
+
+ public InvalidOptionValueException(String option, String value, String description) {
+ super("Option '" + option + "' have invalid value '" + value + "': " + description);
+ setHtmlCode(400);
+ setHtmlStatus("Option '" + option + "' have invalid value '" + value + "'");
+ }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/MissingResourceException.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/MissingResourceException.java
new file mode 100644
index 00000000000..4a0cb76f278
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/MissingResourceException.java
@@ -0,0 +1,11 @@
+// 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.utils.staterestapi.errors;
+
+/**
+ * @author hakon
+ */
+public class MissingResourceException extends StateRestApiException {
+ public MissingResourceException(String resource) {
+ super("Missing resource '" + resource + "'");
+ }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/MissingUnitException.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/MissingUnitException.java
new file mode 100644
index 00000000000..037d82fa0b0
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/MissingUnitException.java
@@ -0,0 +1,21 @@
+// 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.utils.staterestapi.errors;
+
+public class MissingUnitException extends StateRestApiException {
+
+ private static String createMessage(String[] path, int level) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("No such resource '");
+ for (int i=0; i<=level; ++i) {
+ if (i != 0) sb.append('/');
+ sb.append(path[i]);
+ }
+ return sb.append("'.").toString();
+ }
+
+ public MissingUnitException(String[] path, int level) {
+ super(createMessage(path, level));
+ setHtmlCode(404);
+ setHtmlStatus(getMessage());
+ }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/NotMasterException.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/NotMasterException.java
new file mode 100644
index 00000000000..46e964d77ef
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/NotMasterException.java
@@ -0,0 +1,9 @@
+// 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.utils.staterestapi.errors;
+
+public abstract class NotMasterException extends StateRestApiException {
+
+ public NotMasterException(String description) {
+ super(description);
+ }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/OperationNotSupportedForUnitException.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/OperationNotSupportedForUnitException.java
new file mode 100644
index 00000000000..e9ebccbfb66
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/OperationNotSupportedForUnitException.java
@@ -0,0 +1,19 @@
+// 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.utils.staterestapi.errors;
+
+import java.util.Arrays;
+
+public class OperationNotSupportedForUnitException extends StateRestApiException {
+
+ private static String createMessage(String[] path, String description) {
+ return new StringBuilder()
+ .append(Arrays.toString(path)).append(": ").append(description)
+ .toString();
+ }
+
+ public OperationNotSupportedForUnitException(String path[], String description) {
+ super(createMessage(path, description));
+ setHtmlCode(405);
+ setHtmlStatus("Operation not supported for resource");
+ }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/OtherMasterException.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/OtherMasterException.java
new file mode 100644
index 00000000000..4a9c4fc60db
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/OtherMasterException.java
@@ -0,0 +1,16 @@
+// 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.utils.staterestapi.errors;
+
+public class OtherMasterException extends NotMasterException {
+ private final String masterHost;
+ private final int masterPort;
+
+ public OtherMasterException(String masterHost, int masterPort) {
+ super("Cluster controller not master. Use master at " + masterHost + ":" + masterPort + ".");
+ this.masterHost = masterHost;
+ this.masterPort = masterPort;
+ }
+
+ public String getHost() { return masterHost; }
+ public int getPort() { return masterPort; }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/StateRestApiException.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/StateRestApiException.java
new file mode 100644
index 00000000000..6509b31c2c9
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/StateRestApiException.java
@@ -0,0 +1,32 @@
+// 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.utils.staterestapi.errors;
+
+public abstract class StateRestApiException extends Exception {
+ private Integer htmlCode;
+ private String htmlStatus;
+
+ public StateRestApiException(String description) {
+ super(description);
+ }
+
+ /**
+ * If given, this HTML code is set in the response. If not given, a value will
+ * be autogenerated to fit.
+ */
+ public StateRestApiException setHtmlCode(int code) {
+ htmlCode = code;
+ return this;
+ }
+
+ /**
+ * If given, this HTML status string is set in the response. If not given, a value will
+ * be autogenerated to fit.
+ */
+ public StateRestApiException setHtmlStatus(String status) {
+ htmlStatus = status;
+ return this;
+ }
+
+ public Integer getCode() { return htmlCode; }
+ public String getStatus() { return htmlStatus; }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/UnknownMasterException.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/UnknownMasterException.java
new file mode 100644
index 00000000000..408d4c05092
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/UnknownMasterException.java
@@ -0,0 +1,9 @@
+// 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.utils.staterestapi.errors;
+
+public class UnknownMasterException extends NotMasterException {
+
+ public UnknownMasterException() {
+ super("No known master cluster controller currently exists.");
+ }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/package-info.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/package-info.java
new file mode 100644
index 00000000000..6925d804cdb
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/errors/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.utils.staterestapi.errors;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/package-info.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/package-info.java
new file mode 100644
index 00000000000..9111172a601
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/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.utils.staterestapi;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/requests/SetUnitStateRequest.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/requests/SetUnitStateRequest.java
new file mode 100644
index 00000000000..972b0c4b82a
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/requests/SetUnitStateRequest.java
@@ -0,0 +1,32 @@
+// 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.utils.staterestapi.requests;
+
+import com.yahoo.vespa.clustercontroller.utils.staterestapi.errors.InvalidContentException;
+import com.yahoo.vespa.clustercontroller.utils.staterestapi.response.UnitState;
+
+import java.util.Map;
+
+public interface SetUnitStateRequest extends UnitRequest {
+
+ Map<String, UnitState> getNewState();
+
+ enum Condition {
+ FORCE(1), // Don't check for any condition before setting unit state
+ SAFE(2); // Only set condition if it is deemed safe (e.g. redundancy is still ok during upgrade)
+
+ public final int value;
+
+ private Condition(int value) {
+ this.value = value;
+ }
+
+ public static Condition fromString(String value) throws InvalidContentException {
+ try {
+ return Condition.valueOf(value.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ throw new InvalidContentException("Invalid value for my enum Condition: " + value);
+ }
+ }
+ }
+ Condition getCondition();
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/requests/UnitRequest.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/requests/UnitRequest.java
new file mode 100644
index 00000000000..4259e837078
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/requests/UnitRequest.java
@@ -0,0 +1,6 @@
+// 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.utils.staterestapi.requests;
+
+public interface UnitRequest {
+ String[] getUnitPath();
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/requests/UnitStateRequest.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/requests/UnitStateRequest.java
new file mode 100644
index 00000000000..a2b0210362f
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/requests/UnitStateRequest.java
@@ -0,0 +1,7 @@
+// 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.utils.staterestapi.requests;
+
+public interface UnitStateRequest extends UnitRequest {
+ public int getRecursiveLevels();
+
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/requests/package-info.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/requests/package-info.java
new file mode 100644
index 00000000000..bbd1884e56f
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/requests/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.utils.staterestapi.requests;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/CurrentUnitState.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/CurrentUnitState.java
new file mode 100644
index 00000000000..20a1f42794c
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/CurrentUnitState.java
@@ -0,0 +1,8 @@
+// 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.utils.staterestapi.response;
+
+import java.util.Map;
+
+public interface CurrentUnitState {
+ public Map<String, UnitState> getStatePerType();
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/SetResponse.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/SetResponse.java
new file mode 100644
index 00000000000..9f4ecac84d4
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/SetResponse.java
@@ -0,0 +1,28 @@
+// 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.utils.staterestapi.response;
+
+/**
+ * The response of a set operation.
+ * @author dybdahl
+ */
+public class SetResponse {
+ private final String reason;
+ private final boolean wasModified;
+
+ public SetResponse(String reason, boolean wasModified) {
+ this.reason = reason;
+ this.wasModified = wasModified;
+ }
+
+ /**
+ * Indicates if data was modified in a set operation.
+ * @return true if modified.
+ */
+ public boolean getWasModified() { return wasModified; }
+
+ /**
+ * Human readable reason.
+ * @return reason as string
+ */
+ public String getReason() { return reason; }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/SubUnitList.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/SubUnitList.java
new file mode 100644
index 00000000000..9415a0f9953
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/SubUnitList.java
@@ -0,0 +1,10 @@
+// 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.utils.staterestapi.response;
+
+import java.util.Map;
+
+public interface SubUnitList {
+ /** id to link map. */
+ public Map<String, String> getSubUnitLinks();
+ public Map<String, UnitResponse> getSubUnits();
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/UnitAttributes.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/UnitAttributes.java
new file mode 100644
index 00000000000..9c5cb6940b5
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/UnitAttributes.java
@@ -0,0 +1,8 @@
+// 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.utils.staterestapi.response;
+
+import java.util.Map;
+
+public interface UnitAttributes {
+ public Map<String, String> getAttributeValues();
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/UnitMetrics.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/UnitMetrics.java
new file mode 100644
index 00000000000..64b8c88584b
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/UnitMetrics.java
@@ -0,0 +1,8 @@
+// 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.utils.staterestapi.response;
+
+import java.util.Map;
+
+public interface UnitMetrics {
+ public Map<String, Number> getMetricMap();
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/UnitResponse.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/UnitResponse.java
new file mode 100644
index 00000000000..278c3f6dd9b
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/UnitResponse.java
@@ -0,0 +1,11 @@
+// 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.utils.staterestapi.response;
+
+import java.util.Map;
+
+public interface UnitResponse {
+ public UnitAttributes getAttributes();
+ public CurrentUnitState getCurrentState();
+ public Map<String, SubUnitList> getSubUnits();
+ public UnitMetrics getMetrics();
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/UnitState.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/UnitState.java
new file mode 100644
index 00000000000..a4ecf2b09f9
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/UnitState.java
@@ -0,0 +1,7 @@
+// 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.utils.staterestapi.response;
+
+public interface UnitState {
+ public String getId();
+ public String getReason();
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/package-info.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/package-info.java
new file mode 100644
index 00000000000..516e48e8047
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/response/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.utils.staterestapi.response;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/server/JsonReader.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/server/JsonReader.java
new file mode 100644
index 00000000000..a25f29f005c
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/server/JsonReader.java
@@ -0,0 +1,96 @@
+// 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.utils.staterestapi.server;
+
+import com.yahoo.vespa.clustercontroller.utils.communication.http.HttpRequest;
+import com.yahoo.vespa.clustercontroller.utils.staterestapi.errors.InvalidContentException;
+import com.yahoo.vespa.clustercontroller.utils.staterestapi.requests.SetUnitStateRequest;
+import com.yahoo.vespa.clustercontroller.utils.staterestapi.response.UnitState;
+import org.codehaus.jettison.json.JSONArray;
+import org.codehaus.jettison.json.JSONObject;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class JsonReader {
+ private static class UnitStateImpl implements UnitState {
+ private final String id;
+ private final String reason;
+
+ public UnitStateImpl(String id, String reason) {
+ this.id = id;
+ this.reason = reason;
+ }
+
+ @Override
+ public String getId() {
+ return id;
+ }
+
+ @Override
+ public String getReason() {
+ return reason;
+ }
+ }
+
+ static class SetRequestData {
+ final Map<String, UnitState> stateMap;
+ final SetUnitStateRequest.Condition condition;
+ public SetRequestData(Map<String, UnitState> stateMap, SetUnitStateRequest.Condition condition) {
+ this.stateMap = stateMap;
+ this.condition = condition;
+ }
+ }
+
+ public SetRequestData getStateRequestData(HttpRequest request) throws Exception {
+ JSONObject json = new JSONObject(request.getPostContent().toString());
+
+ final SetUnitStateRequest.Condition condition;
+
+ if (json.has("condition")) {
+ condition = SetUnitStateRequest.Condition.valueOf(json.getString("condition"));
+ } else {
+ condition = SetUnitStateRequest.Condition.FORCE;
+ }
+
+ Map<String, UnitState> stateMap = new HashMap<>();
+ if (!json.has("state")) {
+ throw new InvalidContentException("Set state requests must contain a state object");
+ }
+ Object o = json.get("state");
+ if (!(o instanceof JSONObject)) {
+ throw new InvalidContentException("value of state is not a json object");
+ }
+
+ JSONObject state = (JSONObject) o;
+
+ JSONArray stateTypes = state.names();
+ for (int i=0; i<stateTypes.length(); ++i) {
+ o = stateTypes.get(i);
+ String type = (String) o;
+ o = state.get(type);
+ if (!(o instanceof JSONObject)) {
+ throw new InvalidContentException("value of state->" + type + " is not a json object");
+ }
+ JSONObject userState = (JSONObject) o;
+ String code = "up";
+ if (userState.has("state")) {
+ o = userState.get("state");
+ if (!(o instanceof String)) {
+ throw new InvalidContentException("value of state->" + type + "->state is not a string");
+ }
+ code = o.toString();
+ }
+ String reason = "";
+ if (userState.has("reason")) {
+ o = userState.get("reason");
+ if (!(o instanceof String)) {
+ throw new InvalidContentException("value of state->" + type + "->reason is not a string");
+ }
+ reason = o.toString();
+ }
+ stateMap.put(type, new UnitStateImpl(code, reason));
+ }
+ return new SetRequestData(stateMap, condition);
+ }
+
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/server/JsonWriter.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/server/JsonWriter.java
new file mode 100644
index 00000000000..f14637c5fb7
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/server/JsonWriter.java
@@ -0,0 +1,103 @@
+// 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.utils.staterestapi.server;
+
+import com.yahoo.vespa.clustercontroller.utils.staterestapi.response.*;
+import org.codehaus.jettison.json.JSONException;
+import org.codehaus.jettison.json.JSONObject;
+
+import java.util.Map;
+
+public class JsonWriter {
+ private String pathPrefix = "/";
+
+ public JsonWriter() {
+ }
+
+ public void setDefaultPathPrefix(String defaultPathPrefix) {
+ if (defaultPathPrefix.isEmpty() || defaultPathPrefix.charAt(0) != '/') {
+ throw new IllegalArgumentException("Path prefix must start with a slash");
+ }
+ this.pathPrefix = defaultPathPrefix;
+ }
+
+ public JSONObject createJson(UnitResponse data) throws Exception {
+ JSONObject json = new JSONObject();
+ fillInJson(data, json);
+ return json;
+ }
+
+ public void fillInJson(UnitResponse data, JSONObject json) throws Exception {
+ UnitAttributes attributes = data.getAttributes();
+ if (attributes != null) fillInJson(attributes, json);
+ CurrentUnitState stateData = data.getCurrentState();
+ if (stateData != null) fillInJson(stateData, json);
+ UnitMetrics metrics = data.getMetrics();
+ if (metrics != null) fillInJson(metrics, json);
+ Map<String, SubUnitList> subUnits = data.getSubUnits();
+ if (subUnits != null) fillInJson(subUnits, json);
+ }
+
+ public void fillInJson(CurrentUnitState stateData, JSONObject json) throws Exception {
+ JSONObject stateJson = new JSONObject();
+ json.put("state", stateJson);
+ Map<String, UnitState> state = stateData.getStatePerType();
+ for (Map.Entry<String, UnitState> e : state.entrySet()) {
+ String stateType = e.getKey();
+ UnitState unitState = e.getValue();
+ JSONObject stateTypeJson = new JSONObject()
+ .put("state", unitState.getId())
+ .put("reason", unitState.getReason());
+ stateJson.put(stateType, stateTypeJson);
+ }
+ }
+
+ public void fillInJson(UnitMetrics metrics, JSONObject json) throws Exception {
+ JSONObject metricsJson = new JSONObject();
+ for (Map.Entry<String, Number> e : metrics.getMetricMap().entrySet()) {
+ metricsJson.put(e.getKey(), e.getValue());
+ }
+ json.put("metrics", metricsJson);
+ }
+ public void fillInJson(UnitAttributes attributes, JSONObject json) throws Exception {
+ JSONObject attributesJson = new JSONObject();
+ for (Map.Entry<String, String> e : attributes.getAttributeValues().entrySet()) {
+ attributesJson.put(e.getKey(), e.getValue());
+ }
+ json.put("attributes", attributesJson);
+ }
+
+ public void fillInJson(Map<String, SubUnitList> subUnitMap, JSONObject json) throws Exception {
+ for(Map.Entry<String, SubUnitList> e : subUnitMap.entrySet()) {
+ String subUnitType = e.getKey();
+ JSONObject typeJson = new JSONObject();
+ for (Map.Entry<String, String> f : e.getValue().getSubUnitLinks().entrySet()) {
+ JSONObject linkJson = new JSONObject();
+ linkJson.put("link", pathPrefix + "/" + f.getValue());
+ typeJson.put(f.getKey(), linkJson);
+ }
+ for (Map.Entry<String, UnitResponse> f : e.getValue().getSubUnits().entrySet()) {
+ JSONObject subJson = new JSONObject();
+ fillInJson(f.getValue(), subJson);
+ typeJson.put(f.getKey(), subJson);
+ }
+ json.put(subUnitType, typeJson);
+ }
+ }
+
+ public JSONObject createErrorJson(String description) {
+ JSONObject o = new JSONObject();
+ try{
+ o.put("message", description);
+ } catch (JSONException e) {
+ // Can't really do anything if we get an error trying to report an error.
+ }
+ return o;
+ }
+
+ public JSONObject createJson(SetResponse setResponse) throws JSONException {
+ JSONObject jsonObject = new JSONObject();
+ jsonObject.put("wasModified", setResponse.getWasModified());
+ jsonObject.put("reason", setResponse.getReason());
+ return jsonObject;
+ }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/server/RestApiHandler.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/server/RestApiHandler.java
new file mode 100644
index 00000000000..43208237bbe
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/server/RestApiHandler.java
@@ -0,0 +1,166 @@
+// 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.utils.staterestapi.server;
+
+import com.yahoo.log.LogLevel;
+import com.yahoo.yolean.Exceptions;
+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 com.yahoo.vespa.clustercontroller.utils.communication.http.JsonHttpResult;
+import com.yahoo.vespa.clustercontroller.utils.staterestapi.StateRestAPI;
+import com.yahoo.vespa.clustercontroller.utils.staterestapi.errors.*;
+import com.yahoo.vespa.clustercontroller.utils.staterestapi.requests.SetUnitStateRequest;
+import com.yahoo.vespa.clustercontroller.utils.staterestapi.requests.UnitStateRequest;
+import com.yahoo.vespa.clustercontroller.utils.staterestapi.response.*;
+
+import java.util.*;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class RestApiHandler implements HttpRequestHandler {
+ private final static Logger log = Logger.getLogger(RestApiHandler.class.getName());
+
+ private final StateRestAPI restApi;
+ private final JsonWriter jsonWriter;
+ private final JsonReader jsonReader = new JsonReader();
+
+ public RestApiHandler(StateRestAPI restApi) {
+ this.restApi = restApi;
+ this.jsonWriter = new JsonWriter();
+ }
+
+ public RestApiHandler setDefaultPathPrefix(String defaultPathPrefix) {
+ jsonWriter.setDefaultPathPrefix(defaultPathPrefix);
+ return this;
+ }
+
+ private static void logRequestException(HttpRequest request, Exception exception, Level level) {
+ String exceptionString = Exceptions.toMessageString(exception);
+ log.log(level, "Failed to process request with URI path " +
+ request.getPath() + ": " + exceptionString);
+ }
+
+ @Override
+ public HttpResult handleRequest(HttpRequest request) {
+ try{
+ final String[] unitPath = createUnitPath(request);
+ if (request.getHttpOperation().equals(HttpRequest.HttpOp.GET)) {
+ final int recursiveLevel = getRecursiveLevel(request);
+ UnitResponse data = restApi.getState(new UnitStateRequest() {
+ @Override
+ public int getRecursiveLevels() {
+ return recursiveLevel;
+ }
+ @Override
+ public String[] getUnitPath() {
+ return unitPath;
+ }
+ });
+ return new JsonHttpResult().setJson(jsonWriter.createJson(data));
+ } else {
+ final JsonReader.SetRequestData setRequestdata = jsonReader.getStateRequestData(request);
+ SetResponse setResponse = restApi.setUnitState(new SetUnitStateRequest() {
+ @Override
+ public Map<String, UnitState> getNewState() {
+ return setRequestdata.stateMap;
+ }
+ @Override
+ public String[] getUnitPath() {
+ return unitPath;
+ }
+ @Override
+ public Condition getCondition() { return setRequestdata.condition; }
+ });
+ return new JsonHttpResult().setJson(jsonWriter.createJson(setResponse));
+ }
+ } catch (OtherMasterException exception) {
+ logRequestException(request, exception, LogLevel.DEBUG);
+ JsonHttpResult result = new JsonHttpResult();
+ result.setHttpCode(307, "Temporary Redirect");
+ result.addHeader("Location", getMasterLocationUrl(request, exception.getHost(), exception.getPort()));
+ result.setJson(jsonWriter.createErrorJson(exception.getMessage()));
+ return result;
+ } catch (UnknownMasterException exception) {
+ logRequestException(request, exception, Level.WARNING);
+ JsonHttpResult result = new JsonHttpResult();
+ result.setHttpCode(503, "Service Unavailable");
+ result.setJson(jsonWriter.createErrorJson(exception.getMessage()));
+ return result;
+ } catch (StateRestApiException exception) {
+ logRequestException(request, exception, Level.WARNING);
+ JsonHttpResult result = new JsonHttpResult();
+ result.setHttpCode(500, "Failed to process request");
+ if (exception.getStatus() != null) result.setHttpCode(result.getHttpReturnCode(), exception.getStatus());
+ if (exception.getCode() != null) result.setHttpCode(exception.getCode(), result.getHttpReturnCodeDescription());
+ result.setJson(jsonWriter.createErrorJson(exception.getMessage()));
+ return result;
+ } catch (Exception exception) {
+ logRequestException(request, exception, LogLevel.ERROR);
+ JsonHttpResult result = new JsonHttpResult();
+ result.setHttpCode(500, "Failed to process request");
+ result.setJson(jsonWriter.createErrorJson(exception.getClass().getName() + ": " + exception.getMessage()));
+ return result;
+ }
+ }
+
+ private String[] createUnitPath(HttpRequest request) {
+ List<String> path = Arrays.asList(request.getPath().split("/"));
+ return path.subList(3, path.size()).toArray(new String[0]);
+ }
+
+ private int getRecursiveLevel(HttpRequest request) throws StateRestApiException {
+ String val = request.getOption("recursive", "false");
+ if (val.toLowerCase().equals("false")) { return 0; }
+ if (val.toLowerCase().equals("true")) { return Integer.MAX_VALUE; }
+ int level;
+ try{
+ level = Integer.parseInt(val);
+ if (level < 0) throw new NumberFormatException();
+ } catch (NumberFormatException e) {
+ throw new InvalidOptionValueException(
+ "recursive", val, "Recursive option must be true, false, 0 or a positive integer");
+ }
+ return level;
+ }
+
+ private String getMasterLocationUrl(HttpRequest request, String host, int port) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("http://").append(host).append(':').append(port)
+ .append(request.getPath());
+ if (!request.getUrlOptions().isEmpty()) {
+ boolean first = true;
+ for (HttpRequest.KeyValuePair kvp : request.getUrlOptions()) {
+ sb.append(first ? '?' : '&');
+ first = false;
+ sb.append(httpEscape(kvp.getKey())).append('=').append(httpEscape(kvp.getValue()));
+ }
+ }
+ return sb.toString();
+ }
+
+ private static class Escape {
+ public final String pattern;
+ public final String replaceWith;
+
+ public Escape(String pat, String repl) {
+ this.pattern = pat;
+ this.replaceWith = repl;
+ }
+ }
+ private static List<Escape> escapes = new ArrayList<>();
+ static {
+ escapes.add(new Escape("%", "%25"));
+ escapes.add(new Escape(" ", "%20"));
+ escapes.add(new Escape("\\?", "%3F"));
+ escapes.add(new Escape("=", "%3D"));
+ escapes.add(new Escape("\\&", "%26"));
+ }
+
+ private static String httpEscape(String value) {
+ for(Escape e : escapes) {
+ value = value.replaceAll(e.pattern, e.replaceWith);
+ }
+ return value;
+ }
+
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/server/package-info.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/server/package-info.java
new file mode 100644
index 00000000000..f14520b441c
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/staterestapi/server/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.utils.staterestapi.server;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/test/FakeClock.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/test/FakeClock.java
new file mode 100644
index 00000000000..f143c3930bf
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/test/FakeClock.java
@@ -0,0 +1,36 @@
+// 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.utils.test;
+
+import java.util.logging.Logger;
+
+/**
+ * Unit tests want to fast forward time to avoid waiting for time to pass
+ */
+public class FakeClock extends SettableClock {
+ private static final Logger logger = Logger.getLogger(FakeClock.class.getName());
+ protected long currentTime = 1;
+
+ @Override
+ public long getTimeInMillis() {
+ return currentTime;
+ }
+
+ @Override
+ public void adjust(long adjustment) {
+ synchronized (this) {
+ logger.fine("Adjusting clock, adding " + adjustment + " ms to it.");
+ currentTime += adjustment;
+ notifyAll();
+ }
+ }
+
+ @Override
+ public void set(long newTime) {
+ synchronized (this) {
+ if (newTime < currentTime) {
+ // throw new IllegalArgumentException("Clock attempted to be set to go backwards");
+ }
+ currentTime = newTime;
+ }
+ }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/test/SettableClock.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/test/SettableClock.java
new file mode 100644
index 00000000000..09ae6d3d510
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/test/SettableClock.java
@@ -0,0 +1,9 @@
+// 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.utils.test;
+
+import com.yahoo.vespa.clustercontroller.utils.util.Clock;
+
+public abstract class SettableClock extends Clock {
+ public abstract void set(long newTime);
+ public abstract void adjust(long adjustment);
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/test/TestTransport.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/test/TestTransport.java
new file mode 100644
index 00000000000..032ec5bfe12
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/test/TestTransport.java
@@ -0,0 +1,186 @@
+// 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.utils.test;
+
+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.HttpRequestHandler;
+import com.yahoo.vespa.clustercontroller.utils.communication.http.HttpResult;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+/**
+ * This class is a utility for unit tests.. You can register HttpRequestHandler instances in it, and then
+ * you can extract an AsyncHttpClient&lt;HttpResult&gt; instance from it, which you can use to talk to the
+ * registered servers. Thus you can do end to end testing of components talking over HTTP without actually
+ * going through HTTP if you are using the HTTP abstraction layer in communication.http package.
+ */
+public class TestTransport {
+ private static final Logger log = Logger.getLogger(TestTransport.class.getName());
+ private static class Handler {
+ HttpRequestHandler handler;
+ String pathPrefix;
+ Handler(HttpRequestHandler h, String prefix) { this.handler = h; this.pathPrefix = prefix; }
+ }
+ private static class Socket {
+ public final String hostname;
+ public final int port;
+
+ Socket(String hostname, int port) {
+ this.hostname = hostname;
+ this.port = port;
+ }
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof Socket)) return false;
+ Socket other = (Socket) o;
+ return (hostname.equals(other.hostname) && port == other.port);
+ }
+ @Override
+ public int hashCode() {
+ return hostname.hashCode() * port;
+ }
+ }
+ private static class Request {
+ public final HttpRequest request;
+ public final AsyncOperationImpl<HttpResult> result;
+
+ Request(HttpRequest r, AsyncOperationImpl<HttpResult> rr) {
+ this.request = r;
+ this.result = rr;
+ }
+ }
+ private final Map<Socket, List<Handler>> handlers = new HashMap<>();
+ private final LinkedList<Request> requests = new LinkedList<>();
+ private final AsyncHttpClient<HttpResult> client = new AsyncHttpClient<HttpResult>() {
+ @Override
+ public AsyncOperation<HttpResult> execute(HttpRequest r) {
+ log.fine("Queueing request " + r);
+ if (r.getHttpOperation() == null) {
+ r = r.clone();
+ r.setHttpOperation(r.getPostContent() == null ? HttpRequest.HttpOp.GET : HttpRequest.HttpOp.POST);
+ }
+ r.verifyComplete();
+ AsyncOperationImpl<HttpResult> op = new AsyncOperationImpl<>(r.toString());
+ synchronized (requests) {
+ requests.addLast(new Request(r, op));
+ }
+ return op;
+ }
+ @Override
+ public void close() { TestTransport.this.close(); }
+ };
+ private boolean running = true;
+ private final Thread workerThread = new Thread() {
+ @Override
+ public void run() {
+ while (running) {
+ synchronized (requests) {
+ if (requests.isEmpty()) {
+ try {
+ requests.wait(100);
+ } catch (InterruptedException e) { return; }
+ } else {
+ Request request = requests.removeFirst();
+ HttpRequest r = request.request;
+ log.fine("Processing request " + r);
+ HttpRequestHandler handler = getHandler(r);
+ if (handler == null) {
+ if (log.isLoggable(Level.FINE)) {
+ log.fine("Failed to find target for request " + r.toString(true));
+ log.fine("Existing handlers:");
+ for (Socket socket : handlers.keySet()) {
+ log.fine(" Socket " + socket.hostname + ":" + socket.port);
+ for (Handler h : handlers.get(socket)) {
+ log.fine(" " + h.pathPrefix);
+ }
+ }
+ }
+ request.result.setResult(new HttpResult().setHttpCode(
+ 404, "No such server socket with suitable path prefix found open"));
+ } else {
+ try{
+ request.result.setResult(handler.handleRequest(r));
+ } catch (Exception e) {
+ HttpResult result = new HttpResult().setHttpCode(500, e.getMessage());
+ StringWriter sw = new StringWriter();
+ e.printStackTrace(new PrintWriter(sw));
+ result.setContent(sw.toString());
+ request.result.setResult(result);
+ }
+ }
+ //log.fine("Request " + r.toString(true) + " created result " + request.getSecond().getResult().toString(true));
+ }
+ }
+ }
+ }
+ };
+
+ public TestTransport() {
+ workerThread.start();
+ }
+
+ public void close() {
+ if (!running) return;
+ running = false;
+ synchronized (requests) { requests.notifyAll(); }
+ try {
+ workerThread.join();
+ } catch (InterruptedException e) {}
+ }
+
+ /** Get an HTTP client that talks to this test transport layer. */
+ public AsyncHttpClient<HttpResult> getClient() { return client; }
+
+ private HttpRequestHandler getHandler(HttpRequest r) {
+ Socket socket = new Socket(r.getHost(), r.getPort());
+ synchronized (this) {
+ List<Handler> handlerList = handlers.get(socket);
+ if (handlerList == null) {
+ log.fine("No socket match");
+ return null;
+ }
+ log.fine("Socket found");
+ for (Handler h : handlers.get(socket)) {
+ if (r.getPath().length() >= h.pathPrefix.length() && r.getPath().substring(0, h.pathPrefix.length()).equals(h.pathPrefix)) {
+ return h.handler;
+ }
+ }
+ log.fine("No path prefix match");
+ }
+ return null;
+ }
+
+ public void addServer(HttpRequestHandler server, String hostname, int port, String pathPrefix) {
+ Socket socket = new Socket(hostname, port);
+ synchronized (this) {
+ List<Handler> shandlers = handlers.get(socket);
+ if (shandlers == null) {
+ shandlers = new LinkedList<>();
+ handlers.put(socket, shandlers);
+ }
+ shandlers.add(new Handler(server, pathPrefix));
+ }
+ }
+
+ public void removeServer(HttpRequestHandler server, String hostname, int port, String pathPrefix) {
+ Socket socket = new Socket(hostname, port);
+ synchronized (this) {
+ List<Handler> shandlers = handlers.get(socket);
+ if (shandlers == null) return;
+ for (Handler h : shandlers) {
+ if (h.handler == server && h.pathPrefix.equals(pathPrefix)) {
+ shandlers.remove(h);
+ }
+ }
+ }
+ }
+
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/util/CertainlyCloneable.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/util/CertainlyCloneable.java
new file mode 100644
index 00000000000..d8dccfdd836
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/util/CertainlyCloneable.java
@@ -0,0 +1,22 @@
+// 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.utils.util;
+
+/**
+ * To avoid having to catch clone not supported exception everywhere, and create code with lack of
+ * coverage, this class exist to hide the clone not supported exceptions that should never happen.
+ */
+public class CertainlyCloneable<T> implements Cloneable {
+ @Override
+ public Object clone() {
+ try{
+ return callParentClone();
+ } catch (CloneNotSupportedException e) {
+ // Super clone should never throw exception for objects that should certainly be cloneable.
+ throw new Error(e);
+ }
+ }
+
+ protected Object callParentClone() throws CloneNotSupportedException {
+ return super.clone();
+ }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/util/Clock.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/util/Clock.java
new file mode 100644
index 00000000000..fae3983e2d3
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/util/Clock.java
@@ -0,0 +1,11 @@
+// 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.utils.util;
+
+/**
+ * Wrap access to clock so that we can override it in unit tests
+ */
+public class Clock {
+ public long getTimeInMillis() { return System.currentTimeMillis(); }
+
+ public int getTimeInSecs() { return (int)(getTimeInMillis() / 1000); }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/util/ComponentMetricReporter.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/util/ComponentMetricReporter.java
new file mode 100644
index 00000000000..d047dcb6bbb
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/util/ComponentMetricReporter.java
@@ -0,0 +1,55 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * Metric reporter wrapper to add component name prefix and common dimensions.
+ */
+package com.yahoo.vespa.clustercontroller.utils.util;
+
+import java.util.Map;
+import java.util.TreeMap;
+
+public class ComponentMetricReporter implements MetricReporter {
+ private final MetricReporter impl;
+ private final String prefix;
+ private final Map<String, String> defaultDimensions = new TreeMap<>();
+ private Context defaultContext;
+
+ public ComponentMetricReporter(MetricReporter impl, String prefix) {
+ this.impl = impl;
+ this.prefix = prefix;
+ defaultContext = impl.createContext(defaultDimensions);
+ }
+
+ public ComponentMetricReporter addDimension(String key, String value) {
+ defaultDimensions.put(key, value);
+ defaultContext = impl.createContext(defaultDimensions);
+ return this;
+ }
+
+ public void set(String name, Number value) {
+ impl.set(prefix + name, value, defaultContext);
+ }
+
+ public void add(String name, Number value) {
+ impl.add(prefix + name, value, defaultContext);
+ }
+
+ @Override
+ public void set(String name, Number value, Context context) {
+ impl.set(prefix + name, value, context);
+ }
+
+ @Override
+ public void add(String name, Number value, Context context) {
+ impl.add(prefix + name, value, context);
+ }
+
+ @Override
+ public Context createContext(Map<String, ?> stringMap) {
+ if (stringMap == null) return defaultContext;
+ Map<String, Object> m = new TreeMap<>(stringMap);
+ for(String key : defaultDimensions.keySet()) {
+ if (!m.containsKey(key)) m.put(key, defaultDimensions.get(key));
+ }
+ return impl.createContext(m);
+ }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/util/JSONObjectWrapper.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/util/JSONObjectWrapper.java
new file mode 100644
index 00000000000..ca9561ac85e
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/util/JSONObjectWrapper.java
@@ -0,0 +1,26 @@
+// 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.utils.util;
+
+import org.codehaus.jettison.json.JSONException;
+import org.codehaus.jettison.json.JSONObject;
+
+/**
+ * The Jettison json object class has an interface issue where it hides null pointer exceptions
+ * as checked json exceptions. Consequently one has to create catch clauses that code cannot get
+ * into. This class hides those exceptions.
+ *
+ * (Add functions to this wrapper for new functions needing to hide exceptions like this as they are
+ * needed)
+ */
+public class JSONObjectWrapper extends JSONObject {
+
+ @Override
+ public JSONObjectWrapper put(String key, Object value) {
+ try{
+ super.put(key, value);
+ return this;
+ } catch (JSONException e) {
+ throw new NullPointerException(e.getMessage());
+ }
+ }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/util/MetricReporter.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/util/MetricReporter.java
new file mode 100644
index 00000000000..839eebf4bda
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/util/MetricReporter.java
@@ -0,0 +1,17 @@
+// 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.utils.util;
+
+/**
+ * Wrapper for current jdisc metrics, such that applications can report metrics without depending on
+ * the whole container. The apputil project will provide an implementation of this interface that
+ * reports on to injected jdisc implementation.
+ */
+public interface MetricReporter {
+ void set(java.lang.String s, java.lang.Number number, Context context);
+
+ void add(java.lang.String s, java.lang.Number number, Context context);
+
+ Context createContext(java.util.Map<java.lang.String,?> dimensions);
+ static interface Context {
+ }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/util/NoMetricReporter.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/util/NoMetricReporter.java
new file mode 100644
index 00000000000..ffe6027ce4a
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/util/NoMetricReporter.java
@@ -0,0 +1,15 @@
+// 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.utils.util;
+
+import java.util.Map;
+
+public class NoMetricReporter implements MetricReporter {
+ @Override
+ public void set(String s, Number number, Context context) {}
+
+ @Override
+ public void add(String s, Number number, Context context) {}
+
+ @Override
+ public Context createContext(Map<String, ?> stringMap) { return null; }
+}
diff --git a/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/util/package-info.java b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/util/package-info.java
new file mode 100644
index 00000000000..92aae9f4778
--- /dev/null
+++ b/clustercontroller-utils/src/main/java/com/yahoo/vespa/clustercontroller/utils/util/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.utils.util;
+
+import com.yahoo.osgi.annotation.ExportPackage;