summaryrefslogtreecommitdiffstats
path: root/container-core/src/test/java/com/yahoo/container/jdisc
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 /container-core/src/test/java/com/yahoo/container/jdisc
Publish
Diffstat (limited to 'container-core/src/test/java/com/yahoo/container/jdisc')
-rw-r--r--container-core/src/test/java/com/yahoo/container/jdisc/ExtendedResponseTestCase.java89
-rw-r--r--container-core/src/test/java/com/yahoo/container/jdisc/HttpRequestTestCase.java104
-rw-r--r--container-core/src/test/java/com/yahoo/container/jdisc/HttpResponseTestCase.java82
-rw-r--r--container-core/src/test/java/com/yahoo/container/jdisc/LoggingRequestHandlerTestCase.java224
-rw-r--r--container-core/src/test/java/com/yahoo/container/jdisc/LoggingTestCase.java109
-rw-r--r--container-core/src/test/java/com/yahoo/container/jdisc/RequestBuilderTestCase.java46
-rw-r--r--container-core/src/test/java/com/yahoo/container/jdisc/ThreadedRequestHandlerTestCase.java317
-rw-r--r--container-core/src/test/java/com/yahoo/container/jdisc/state/MetricConsumerProviders.java21
-rw-r--r--container-core/src/test/java/com/yahoo/container/jdisc/state/MetricSnapshotTest.java23
-rw-r--r--container-core/src/test/java/com/yahoo/container/jdisc/state/StateHandlerTest.java409
-rw-r--r--container-core/src/test/java/com/yahoo/container/jdisc/state/StateMonitorBenchmarkTest.java80
11 files changed, 1504 insertions, 0 deletions
diff --git a/container-core/src/test/java/com/yahoo/container/jdisc/ExtendedResponseTestCase.java b/container-core/src/test/java/com/yahoo/container/jdisc/ExtendedResponseTestCase.java
new file mode 100644
index 00000000000..78424b2f4e0
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/jdisc/ExtendedResponseTestCase.java
@@ -0,0 +1,89 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.jdisc;
+
+import static org.junit.Assert.*;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.handler.CompletionHandler;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.text.Utf8;
+
+/**
+ * API test for ExtendedResponse.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class ExtendedResponseTestCase {
+
+ private static final String COM_YAHOO_CONTAINER_JDISC_EXTENDED_RESPONSE_TEST_CASE = "com.yahoo.container.jdisc.ExtendedResponseTestCase";
+ ExtendedResponse r;
+
+ private static class TestResponse extends ExtendedResponse {
+
+ public TestResponse(int status) {
+ super(status);
+ }
+
+
+ @Override
+ public void render(OutputStream output, ContentChannel networkChannel,
+ CompletionHandler handler) throws IOException {
+ // yes, this is sync rendering, so sue me :p
+ try {
+ output.write(Utf8.toBytes(COM_YAHOO_CONTAINER_JDISC_EXTENDED_RESPONSE_TEST_CASE));
+ } finally {
+ if (networkChannel != null) {
+ networkChannel.close(handler);
+ }
+ }
+ }
+ }
+
+
+ @Before
+ public void setUp() throws Exception {
+ r = new TestResponse(Response.Status.OK);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ r = null;
+ }
+
+ @Test
+ public final void testRenderOutputStreamContentChannelCompletionHandler() throws IOException {
+ ByteArrayOutputStream b = new ByteArrayOutputStream();
+ r.render(b, null, null);
+ assertEquals(COM_YAHOO_CONTAINER_JDISC_EXTENDED_RESPONSE_TEST_CASE, Utf8.toString(b.toByteArray()));
+ }
+
+
+ @Test
+ public final void testGetParsedQuery() {
+ assertNull(r.getParsedQuery());
+ }
+
+ @Test
+ public final void testGetTiming() {
+ assertNull(r.getTiming());
+ }
+
+ @Test
+ public final void testGetCoverage() {
+ assertNull(r.getCoverage());
+ }
+
+ @Test
+ public final void testGetHitCounts() {
+ assertNull(r.getHitCounts());
+ }
+
+}
diff --git a/container-core/src/test/java/com/yahoo/container/jdisc/HttpRequestTestCase.java b/container-core/src/test/java/com/yahoo/container/jdisc/HttpRequestTestCase.java
new file mode 100644
index 00000000000..6434c1c3e7e
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/jdisc/HttpRequestTestCase.java
@@ -0,0 +1,104 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.jdisc;
+
+import static org.junit.Assert.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collections;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.yahoo.jdisc.http.HttpRequest.Method;
+import com.yahoo.text.Utf8;
+
+/**
+ * API control of HttpRequest.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class HttpRequestTestCase {
+ private static final String X_RAY_YANKEE_ZULU = "x-ray yankee zulu";
+ private static final String HTTP_MAILHOST_25_ALPHA_BRAVO_CHARLIE_DELTA = "http://mailhost:25/alpha?bravo=charlie&falseboolean=false&trueboolean=true";
+ HttpRequest r;
+ InputStream requestData;
+
+ @Before
+ public void setUp() throws Exception {
+ requestData = new ByteArrayInputStream(Utf8.toBytes(X_RAY_YANKEE_ZULU));
+ r = HttpRequest.createTestRequest(HTTP_MAILHOST_25_ALPHA_BRAVO_CHARLIE_DELTA, Method.GET, requestData, Collections.singletonMap("foxtrot", "golf"));
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ r = null;
+ }
+
+ @Test
+ public final void testGetMethod() {
+ assertSame(Method.GET, r.getMethod());
+ }
+
+ @Test
+ public final void testGetUri() throws URISyntaxException {
+ assertEquals(new URI(HTTP_MAILHOST_25_ALPHA_BRAVO_CHARLIE_DELTA), r.getUri());
+ }
+
+ @Test
+ public final void testGetJDiscRequest() throws URISyntaxException {
+ assertEquals(new URI(HTTP_MAILHOST_25_ALPHA_BRAVO_CHARLIE_DELTA), r.getJDiscRequest().getUri());
+ }
+
+ @Test
+ public final void testGetProperty() {
+ assertEquals("charlie", r.getProperty("bravo"));
+ assertEquals("golf", r.getProperty("foxtrot"));
+ assertNull(r.getProperty("zulu"));
+ }
+
+ @Test
+ public final void testPropertyMap() {
+ assertEquals(4, r.propertyMap().size());
+ }
+
+ @Test
+ public final void testGetBooleanProperty() {
+ assertTrue(r.getBooleanProperty("trueboolean"));
+ assertFalse(r.getBooleanProperty("falseboolean"));
+ assertFalse(r.getBooleanProperty("bravo"));
+ }
+
+ @Test
+ public final void testHasProperty() {
+ assertFalse(r.hasProperty("alpha"));
+ assertTrue(r.hasProperty("bravo"));
+ }
+
+ @Test
+ public final void testGetHeader() {
+ assertNull(r.getHeader("SyntheticHeaderFor-com.yahoo.container.jdisc.HttpRequestTestCase"));
+ }
+
+ @Test
+ public final void testGetHost() {
+ assertEquals("mailhost", r.getHost());
+ }
+
+ @Test
+ public final void testGetPort() {
+ assertEquals(25, r.getPort());
+ }
+
+ @Test
+ public final void testGetData() throws IOException {
+ byte[] b = new byte[X_RAY_YANKEE_ZULU.length()];
+ r.getData().read(b);
+ assertEquals(X_RAY_YANKEE_ZULU, Utf8.toString(b));
+ }
+
+}
diff --git a/container-core/src/test/java/com/yahoo/container/jdisc/HttpResponseTestCase.java b/container-core/src/test/java/com/yahoo/container/jdisc/HttpResponseTestCase.java
new file mode 100644
index 00000000000..6349da6e771
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/jdisc/HttpResponseTestCase.java
@@ -0,0 +1,82 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.jdisc;
+
+import static org.junit.Assert.*;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.yahoo.jdisc.Response;
+import com.yahoo.text.Utf8;
+
+/**
+ * API test for HttpResponse.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class HttpResponseTestCase {
+
+ private static final String COM_YAHOO_CONTAINER_JDISC_HTTP_RESPONSE_TEST_CASE_TEST_RESPONSE = "com.yahoo.container.jdisc.HttpResponseTestCase.TestResponse";
+
+ private static class TestResponse extends HttpResponse {
+
+ public TestResponse(int status) {
+ super(status);
+ }
+
+ @Override
+ public void render(OutputStream outputStream) throws IOException {
+ outputStream.write(Utf8.toBytes(COM_YAHOO_CONTAINER_JDISC_HTTP_RESPONSE_TEST_CASE_TEST_RESPONSE));
+ }
+ }
+
+ HttpResponse r;
+
+ @Before
+ public void setUp() throws Exception {
+ r = new TestResponse(Response.Status.OK);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ r = null;
+ }
+
+ @Test
+ public final void testRender() throws IOException {
+ ByteArrayOutputStream o = new ByteArrayOutputStream(1024);
+ r.render(o);
+ assertEquals(COM_YAHOO_CONTAINER_JDISC_HTTP_RESPONSE_TEST_CASE_TEST_RESPONSE, Utf8.toString(o.toByteArray()));
+ }
+
+ @Test
+ public final void testGetStatus() {
+ assertEquals(Response.Status.OK, r.getStatus());
+ }
+
+ @Test
+ public final void testHeaders() {
+ assertNotNull(r.headers());
+ }
+
+ @Test
+ public final void testGetJdiscResponse() {
+ assertNotNull(r.getJdiscResponse());
+ }
+
+ @Test
+ public final void testGetContentType() {
+ assertEquals(HttpResponse.DEFAULT_MIME_TYPE, r.getContentType());
+ }
+
+ @Test
+ public final void testGetCharacterEncoding() {
+ assertEquals(HttpResponse.DEFAULT_CHARACTER_ENCODING, r.getCharacterEncoding());
+ }
+
+}
diff --git a/container-core/src/test/java/com/yahoo/container/jdisc/LoggingRequestHandlerTestCase.java b/container-core/src/test/java/com/yahoo/container/jdisc/LoggingRequestHandlerTestCase.java
new file mode 100644
index 00000000000..d2d74502102
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/jdisc/LoggingRequestHandlerTestCase.java
@@ -0,0 +1,224 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.jdisc;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.URISyntaxException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import com.google.inject.Key;
+import com.yahoo.container.logging.HitCounts;
+import com.yahoo.jdisc.Container;
+import com.yahoo.jdisc.References;
+import com.yahoo.jdisc.ResourceReference;
+import com.yahoo.jdisc.handler.RequestHandler;
+import com.yahoo.jdisc.service.CurrentContainer;
+import java.net.URI;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.provider.ComponentRegistry;
+import com.yahoo.container.handler.Coverage;
+import com.yahoo.container.handler.Timing;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.container.logging.AccessLogEntry;
+import com.yahoo.container.logging.AccessLogInterface;
+import com.yahoo.jdisc.handler.BufferedContentChannel;
+import com.yahoo.jdisc.handler.CompletionHandler;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.ResponseHandler;
+
+/**
+ * Test contracts in LoggingRequestHandler.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class LoggingRequestHandlerTestCase {
+
+ StartTimePusher accessLogging;
+ AccessLogTestHandler handler;
+ ExecutorService executor;
+
+ public static final class NoTimingResponse extends ExtendedResponse {
+
+ public NoTimingResponse() {
+ super(200);
+ }
+
+
+ @Override
+ public HitCounts getHitCounts() {
+ return new HitCounts(1, 1, 1, 1, 1);
+ }
+
+ @Override
+ public Timing getTiming() {
+ return null;
+ }
+
+ @Override
+ public Coverage getCoverage() {
+ return new Coverage(1, 1, true);
+ }
+
+
+ @Override
+ public void render(OutputStream output, ContentChannel networkChannel,
+ CompletionHandler handler) throws IOException {
+ networkChannel.close(handler);
+ }
+ }
+
+ static class CloseableContentChannel implements ContentChannel {
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ if (handler != null) {
+ handler.completed();
+ }
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ if (handler != null) {
+ handler.completed();
+ }
+ }
+
+ }
+
+ public static final class MockResponseHandler implements ResponseHandler {
+ public final ContentChannel channel = new CloseableContentChannel();
+
+ @Override
+ public ContentChannel handleResponse(
+ final com.yahoo.jdisc.Response response) {
+ return channel;
+ }
+ }
+
+ static final class AccessLogTestHandler extends LoggingRequestHandler {
+
+ public AccessLogTestHandler(Executor executor, AccessLog accessLog) {
+ super(executor, accessLog);
+ }
+
+ @Override
+ public HttpResponse handle(HttpRequest request) {
+ return new NoTimingResponse();
+ }
+
+ }
+
+ static final class StartTimePusher implements AccessLogInterface {
+
+ public final ArrayBlockingQueue<Long> starts = new ArrayBlockingQueue<>(1);
+
+ @Override
+ public void log(final AccessLogEntry accessLogEntry) {
+ starts.offer(Long.valueOf(accessLogEntry.getTimeStampMillis()));
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ accessLogging = new StartTimePusher();
+ ComponentRegistry<AccessLogInterface> implementers = new ComponentRegistry<>();
+ implementers.register(new ComponentId("nalle"), accessLogging);
+ implementers.freeze();
+ executor = Executors.newCachedThreadPool();
+ handler = new AccessLogTestHandler(executor, new AccessLog(implementers));
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ accessLogging = null;
+ handler = null;
+ executor.shutdown();
+ executor = null;
+ }
+
+ @Test
+ public final void checkStartIsNotZeroWithoutTimingInstance() throws InterruptedException {
+ Long startTime;
+
+ MockResponseHandler responseHandler = new MockResponseHandler();
+ com.yahoo.jdisc.http.HttpRequest request = createRequest();
+ BufferedContentChannel requestContent = new BufferedContentChannel();
+ requestContent.close(null);
+ handler.handleRequest(request, requestContent, responseHandler);
+ startTime = accessLogging.starts.poll(5, TimeUnit.MINUTES);
+ if (startTime == null) {
+ // test timed out, ignoring
+ } else {
+ assertFalse(
+ "Start time was 0, that should never happen after the first millisecond of 1970.",
+ startTime.longValue() == 0L);
+ }
+ }
+
+ public static com.yahoo.jdisc.http.HttpRequest createRequest() {
+ return createRequest("http://localhost/search/?query=geewhiz");
+ }
+
+ public static com.yahoo.jdisc.http.HttpRequest createRequest(String uri) {
+ com.yahoo.jdisc.http.HttpRequest request = null;
+ try {
+ request = com.yahoo.jdisc.http.HttpRequest.newClientRequest(new com.yahoo.jdisc.Request(new MockCurrentContainer(), new URI(uri)), new URI(uri),
+ com.yahoo.jdisc.http.HttpRequest.Method.GET, com.yahoo.jdisc.http.HttpRequest.Version.HTTP_1_1);
+ request.setRemoteAddress(new InetSocketAddress(0));
+ } catch (URISyntaxException e) {
+ fail("Illegal URI string in test?");
+ }
+ return request;
+ }
+
+ private static class MockCurrentContainer implements CurrentContainer {
+ @Override
+ public Container newReference(java.net.URI uri) {
+ return new Container() {
+
+ @Override
+ public RequestHandler resolveHandler(com.yahoo.jdisc.Request request) {
+ return null;
+ }
+
+ @Override
+ public <T> T getInstance(Key<T> tKey) {
+ return null;
+ }
+
+ @Override
+ public <T> T getInstance(Class<T> tClass) {
+ return null;
+ }
+
+ @Override
+ public ResourceReference refer() {
+ return References.NOOP_REFERENCE;
+ }
+
+ @Override
+ public void release() {
+ // NOP
+ }
+
+ @Override
+ public long currentTimeMillis() {
+ return 37;
+ }
+ };
+ }
+ }
+
+}
diff --git a/container-core/src/test/java/com/yahoo/container/jdisc/LoggingTestCase.java b/container-core/src/test/java/com/yahoo/container/jdisc/LoggingTestCase.java
new file mode 100644
index 00000000000..6fc93c2eea4
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/jdisc/LoggingTestCase.java
@@ -0,0 +1,109 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.jdisc;
+
+import com.yahoo.jdisc.handler.CompletionHandler;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.log.LogLevel;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+/**
+ * Check error logging from ContentChannelOutputStream is sane.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class LoggingTestCase {
+
+ Logger logger = Logger.getLogger(ContentChannelOutputStream.class.getName());
+ boolean initUseParentHandlers = logger.getUseParentHandlers();
+ LogCheckHandler logChecker;
+ Level initLevel;
+
+ private static class FailingContentChannel implements ContentChannel {
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ handler.failed(new RuntimeException());
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ // NOP
+
+ }
+ }
+
+ private class LogCheckHandler extends Handler {
+ Map<Level, Integer> errorCounter = new HashMap<>();
+
+ @Override
+ public void publish(LogRecord record) {
+ synchronized (errorCounter) {
+ Integer count = errorCounter.get(record.getLevel());
+ if (count == null) {
+ count = Integer.valueOf(0);
+ }
+ errorCounter.put(record.getLevel(),
+ Integer.valueOf(count.intValue() + 1));
+ }
+ }
+
+ @Override
+ public void flush() {
+ }
+
+ @Override
+ public void close() throws SecurityException {
+ }
+ }
+
+ ContentChannelOutputStream stream;
+
+ @Before
+ public void setUp() throws Exception {
+ stream = new ContentChannelOutputStream(new FailingContentChannel());
+ logger = Logger.getLogger(ContentChannelOutputStream.class.getName());
+ initUseParentHandlers = logger.getUseParentHandlers();
+ logger.setUseParentHandlers(false);
+ logger.setLevel(Level.ALL);
+ logChecker = new LogCheckHandler();
+ logger.addHandler(logChecker);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ logger.removeHandler(logChecker);
+ logger.setUseParentHandlers(initUseParentHandlers);
+ logger.setLevel(initLevel);
+ }
+
+ private ByteBuffer createData() {
+ ByteBuffer b = ByteBuffer.allocate(10);
+ return b;
+ }
+
+ @Test
+ public final void testFailed() throws IOException, InterruptedException {
+ stream.send(createData());
+ stream.send(createData());
+ stream.send(createData());
+ stream.flush();
+ assertNull(logChecker.errorCounter.get(LogLevel.INFO));
+ assertEquals(1, logChecker.errorCounter.get(LogLevel.DEBUG).intValue());
+ assertEquals(2, logChecker.errorCounter.get(LogLevel.SPAM).intValue());
+ }
+
+}
diff --git a/container-core/src/test/java/com/yahoo/container/jdisc/RequestBuilderTestCase.java b/container-core/src/test/java/com/yahoo/container/jdisc/RequestBuilderTestCase.java
new file mode 100644
index 00000000000..375e7fe39f5
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/jdisc/RequestBuilderTestCase.java
@@ -0,0 +1,46 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.jdisc;
+
+import static org.junit.Assert.*;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.yahoo.jdisc.http.HttpRequest.Method;
+
+/**
+ * API check for HttpRequest.Builder.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class RequestBuilderTestCase {
+ HttpRequest.Builder b;
+
+ @Before
+ public void setUp() throws Exception {
+ HttpRequest r = HttpRequest.createTestRequest("http://ssh:22/alpha?bravo=charlie", Method.GET);
+ b = new HttpRequest.Builder(r);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ b = null;
+ }
+
+ @Test
+ public final void testBasic() {
+ HttpRequest r = b.put("delta", "echo").createDirectRequest();
+ assertEquals("charlie", r.getProperty("bravo"));
+ assertEquals("echo", r.getProperty("delta"));
+ }
+
+ @Test
+ public void testRemove() {
+ HttpRequest orig = b.put("delta", "echo").createDirectRequest();
+
+ HttpRequest child = new HttpRequest.Builder(orig).removeProperty("delta").createDirectRequest();
+ assertFalse(child.propertyMap().containsKey("delta"));
+ }
+
+}
diff --git a/container-core/src/test/java/com/yahoo/container/jdisc/ThreadedRequestHandlerTestCase.java b/container-core/src/test/java/com/yahoo/container/jdisc/ThreadedRequestHandlerTestCase.java
new file mode 100644
index 00000000000..400cb507620
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/jdisc/ThreadedRequestHandlerTestCase.java
@@ -0,0 +1,317 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.jdisc;
+
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.application.ContainerBuilder;
+import com.yahoo.jdisc.handler.*;
+import com.yahoo.jdisc.test.TestDriver;
+import org.junit.Test;
+
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.*;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ThreadedRequestHandlerTestCase {
+
+ @Test
+ public void requireThatNullExecutorThrowsException() {
+ try {
+ new ThreadedRequestHandler(null) {
+
+ @Override
+ public void handleRequest(Request request, BufferedContentChannel content, ResponseHandler handler) {
+
+ }
+ };
+ fail();
+ } catch (NullPointerException e) {
+
+ }
+ }
+
+ @Test
+ public void requireThatHandlerSetsRequestTimeout() throws InterruptedException {
+ Executor executor = Executors.newSingleThreadExecutor();
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ MyRequestHandler requestHandler = MyRequestHandler.newInstance(executor);
+ builder.serverBindings().bind("http://localhost/", requestHandler);
+ driver.activateContainer(builder);
+
+ MyResponseHandler responseHandler = new MyResponseHandler();
+ driver.dispatchRequest("http://localhost/", responseHandler);
+
+ requestHandler.entryLatch.countDown();
+ assertTrue(requestHandler.exitLatch.await(60, TimeUnit.SECONDS));
+ assertNull(requestHandler.content.read());
+ assertNotNull(requestHandler.request.getTimeout(TimeUnit.MILLISECONDS));
+
+ assertTrue(responseHandler.latch.await(60, TimeUnit.SECONDS));
+ assertNull(responseHandler.content.read());
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatRequestAndResponseReachHandlers() throws InterruptedException {
+ Executor executor = Executors.newSingleThreadExecutor();
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ MyRequestHandler requestHandler = MyRequestHandler.newInstance(executor);
+ builder.serverBindings().bind("http://localhost/", requestHandler);
+ driver.activateContainer(builder);
+
+ MyResponseHandler responseHandler = new MyResponseHandler();
+ Request request = new Request(driver, URI.create("http://localhost/"));
+ ContentChannel requestContent = request.connect(responseHandler);
+ ByteBuffer buf = ByteBuffer.allocate(69);
+ requestContent.write(buf, null);
+ requestContent.close(null);
+ request.release();
+
+ requestHandler.entryLatch.countDown();
+ assertTrue(requestHandler.exitLatch.await(60, TimeUnit.SECONDS));
+ assertSame(request, requestHandler.request);
+ assertSame(buf, requestHandler.content.read());
+ assertNull(requestHandler.content.read());
+
+ assertTrue(responseHandler.latch.await(60, TimeUnit.SECONDS));
+ assertSame(requestHandler.response, responseHandler.response);
+ assertNull(responseHandler.content.read());
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatRejectedExecutionIsHandledGracefully() throws Exception {
+ // Instrumentation.
+ final Executor executor = new Executor() {
+ @Override
+ public void execute(final Runnable command) {
+ throw new RejectedExecutionException("Deliberately thrown; simulating overloaded executor");
+ }
+ };
+ final RequestHandler requestHandler = new ThreadedRequestHandler(executor) {
+ @Override
+ protected void handleRequest(Request request, BufferedContentChannel requestContent, ResponseHandler responseHandler) {
+ throw new AssertionError("Should never get here");
+ }
+ };
+
+ // Setup.
+ final TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ final ContainerBuilder builder = driver.newContainerBuilder();
+ builder.serverBindings().bind("http://localhost/", requestHandler);
+ driver.activateContainer(builder);
+ final MyResponseHandler responseHandler = new MyResponseHandler();
+
+ // Execution.
+ try {
+ driver.dispatchRequest("http://localhost/", responseHandler);
+ fail("Above statement should throw exception");
+ } catch (OverloadException e) {
+ // As expected.
+ }
+
+ // Verification.
+ assertEquals("Response handler should be invoked synchronously in this case.", 0, responseHandler.latch.getCount());
+ assertEquals(Response.Status.SERVICE_UNAVAILABLE, responseHandler.response.getStatus());
+ assertNull(responseHandler.content.read());
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatRequestContentIsClosedIfHandlerIgnoresIt() throws InterruptedException {
+ Executor executor = Executors.newSingleThreadExecutor();
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ MyRequestHandler requestHandler = MyRequestHandler.newIgnoreContent(executor);
+ builder.serverBindings().bind("http://localhost/", requestHandler);
+ driver.activateContainer(builder);
+
+ MyResponseHandler responseHandler = new MyResponseHandler();
+ ContentChannel content = driver.connectRequest("http://localhost/", responseHandler);
+ MyCompletion writeCompletion = new MyCompletion();
+ content.write(ByteBuffer.allocate(69), writeCompletion);
+ MyCompletion closeCompletion = new MyCompletion();
+ content.close(closeCompletion);
+
+ requestHandler.entryLatch.countDown();
+ assertTrue(requestHandler.exitLatch.await(60, TimeUnit.SECONDS));
+ assertTrue(writeCompletion.latch.await(60, TimeUnit.SECONDS));
+ assertTrue(writeCompletion.completed);
+ assertTrue(closeCompletion.latch.await(60, TimeUnit.SECONDS));
+ assertTrue(writeCompletion.completed);
+
+ assertTrue(responseHandler.latch.await(60, TimeUnit.SECONDS));
+ assertSame(requestHandler.response, responseHandler.response);
+ assertNull(responseHandler.content.read());
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatResponseIsDispatchedIfHandlerIgnoresIt() throws InterruptedException {
+ Executor executor = Executors.newSingleThreadExecutor();
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ MyRequestHandler requestHandler = MyRequestHandler.newIgnoreResponse(executor);
+ builder.serverBindings().bind("http://localhost/", requestHandler);
+ driver.activateContainer(builder);
+
+ MyResponseHandler responseHandler = new MyResponseHandler();
+ driver.dispatchRequest("http://localhost/", responseHandler);
+ requestHandler.entryLatch.countDown();
+ assertTrue(requestHandler.exitLatch.await(60, TimeUnit.SECONDS));
+ assertNull(requestHandler.content.read());
+
+ assertTrue(responseHandler.latch.await(60, TimeUnit.SECONDS));
+ assertEquals(Response.Status.INTERNAL_SERVER_ERROR, responseHandler.response.getStatus());
+ assertNull(responseHandler.content.read());
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void requireThatRequestContentIsClosedAndResponseIsDispatchedIfHandlerIgnoresIt()
+ throws InterruptedException
+ {
+ Executor executor = Executors.newSingleThreadExecutor();
+ assertThatRequestContentIsClosedAndResponseIsDispatchedIfHandlerIgnoresIt(
+ MyRequestHandler.newIgnoreAll(executor));
+ assertThatRequestContentIsClosedAndResponseIsDispatchedIfHandlerIgnoresIt(
+ MyRequestHandler.newThrowException(executor));
+ }
+
+ private static void assertThatRequestContentIsClosedAndResponseIsDispatchedIfHandlerIgnoresIt(
+ MyRequestHandler requestHandler)
+ throws InterruptedException
+ {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi();
+ ContainerBuilder builder = driver.newContainerBuilder();
+ builder.serverBindings().bind("http://localhost/", requestHandler);
+ driver.activateContainer(builder);
+
+ MyResponseHandler responseHandler = new MyResponseHandler();
+ ContentChannel content = driver.connectRequest("http://localhost/", responseHandler);
+ MyCompletion writeCompletion = new MyCompletion();
+ content.write(ByteBuffer.allocate(69), writeCompletion);
+ MyCompletion closeCompletion = new MyCompletion();
+ content.close(closeCompletion);
+
+ requestHandler.entryLatch.countDown();
+ assertTrue(requestHandler.exitLatch.await(60, TimeUnit.SECONDS));
+ assertTrue(writeCompletion.latch.await(60, TimeUnit.SECONDS));
+ assertTrue(writeCompletion.completed);
+ assertTrue(closeCompletion.latch.await(60, TimeUnit.SECONDS));
+ assertTrue(writeCompletion.completed);
+
+ assertTrue(responseHandler.latch.await(60, TimeUnit.SECONDS));
+ assertEquals(Response.Status.INTERNAL_SERVER_ERROR, responseHandler.response.getStatus());
+ assertNull(responseHandler.content.read());
+ assertTrue(driver.close());
+ }
+
+ private static class MyRequestHandler extends ThreadedRequestHandler {
+
+ final CountDownLatch entryLatch = new CountDownLatch(1);
+ final CountDownLatch exitLatch = new CountDownLatch(1);
+ final ReadableContentChannel content = new ReadableContentChannel();
+ final boolean consumeContent;
+ final boolean createResponse;
+ final boolean throwException;
+ Response response = null;
+ Request request = null;
+
+ MyRequestHandler(Executor executor, boolean consumeContent, boolean createResponse, boolean throwException) {
+ super(executor);
+ this.consumeContent = consumeContent;
+ this.createResponse = createResponse;
+ this.throwException = throwException;
+ }
+
+ @Override
+ public void handleRequest(Request request, BufferedContentChannel content, ResponseHandler handler) {
+ try {
+ if (!entryLatch.await(60, TimeUnit.SECONDS)) {
+ return;
+ }
+ if (throwException) {
+ throw new RuntimeException();
+ }
+ this.request = request;
+ if (consumeContent) {
+ content.connectTo(this.content);
+ }
+ if (createResponse) {
+ response = new Response(Response.Status.OK);
+ handler.handleResponse(response).close(null);
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } finally {
+ exitLatch.countDown();
+ }
+ }
+
+ static MyRequestHandler newInstance(Executor executor) {
+ return new MyRequestHandler(executor, true, true, false);
+ }
+
+ static MyRequestHandler newThrowException(Executor executor) {
+ return new MyRequestHandler(executor, true, true, true);
+ }
+
+ static MyRequestHandler newIgnoreContent(Executor executor) {
+ return new MyRequestHandler(executor, false, true, false);
+ }
+
+ static MyRequestHandler newIgnoreResponse(Executor executor) {
+ return new MyRequestHandler(executor, true, false, false);
+ }
+
+ static MyRequestHandler newIgnoreAll(Executor executor) {
+ return new MyRequestHandler(executor, false, false, false);
+ }
+ }
+
+ private static class MyResponseHandler implements ResponseHandler {
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ReadableContentChannel content = new ReadableContentChannel();
+ Response response = null;
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ this.response = response;
+ latch.countDown();
+
+ BufferedContentChannel content = new BufferedContentChannel();
+ content.connectTo(this.content);
+ return content;
+ }
+ }
+
+ private static class MyCompletion implements CompletionHandler {
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ boolean completed;
+
+ @Override
+ public void completed() {
+ completed = true;
+ latch.countDown();
+ }
+
+ @Override
+ public void failed(Throwable t) {
+ latch.countDown();
+ }
+ }
+}
diff --git a/container-core/src/test/java/com/yahoo/container/jdisc/state/MetricConsumerProviders.java b/container-core/src/test/java/com/yahoo/container/jdisc/state/MetricConsumerProviders.java
new file mode 100644
index 00000000000..ff62f1f4078
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/jdisc/state/MetricConsumerProviders.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.container.jdisc.state;
+
+import com.google.inject.Provider;
+import com.yahoo.jdisc.application.MetricConsumer;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+class MetricConsumerProviders {
+
+ public static Provider<MetricConsumer> wrap(final StateMonitor statetMonitor) {
+ return new Provider<MetricConsumer>() {
+
+ @Override
+ public MetricConsumer get() {
+ return statetMonitor.newMetricConsumer();
+ }
+ };
+ }
+}
diff --git a/container-core/src/test/java/com/yahoo/container/jdisc/state/MetricSnapshotTest.java b/container-core/src/test/java/com/yahoo/container/jdisc/state/MetricSnapshotTest.java
new file mode 100644
index 00000000000..9dc9379e585
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/jdisc/state/MetricSnapshotTest.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.container.jdisc.state;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class MetricSnapshotTest {
+ /**
+ * Aggregate metrics are not cloned into new snapshot. In turn, a metric
+ * set with only aggregates will be added as an empty set if we do not
+ * filter these away at clone time. This test ensures that we do just that.
+ * If/when we start carrying aggregates across snapshots, this test will
+ * most likely be deprecated.
+ */
+ @Test
+ public void emptyMetricSetNotAddedToClonedSnapshot() {
+ final StateMetricContext ctx = StateMetricContext.newInstance(null);
+ MetricSnapshot snapshot = new MetricSnapshot();
+ snapshot.add(ctx, "foo", 1234);
+ MetricSnapshot newSnapshot = snapshot.createSnapshot();
+ assertFalse(newSnapshot.iterator().hasNext());
+ }
+}
diff --git a/container-core/src/test/java/com/yahoo/container/jdisc/state/StateHandlerTest.java b/container-core/src/test/java/com/yahoo/container/jdisc/state/StateHandlerTest.java
new file mode 100644
index 00000000000..49e97fc9d06
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/jdisc/state/StateHandlerTest.java
@@ -0,0 +1,409 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.jdisc.state;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.inject.AbstractModule;
+import com.yahoo.container.core.ApplicationMetadataConfig;
+import com.yahoo.container.jdisc.config.HealthMonitorConfig;
+import com.yahoo.jdisc.Metric;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.Timer;
+import com.yahoo.jdisc.application.ContainerBuilder;
+import com.yahoo.jdisc.application.MetricConsumer;
+import com.yahoo.jdisc.handler.BufferedContentChannel;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.test.TestDriver;
+import com.yahoo.vespa.defaults.Defaults;
+import org.junit.After;
+import org.junit.Test;
+
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class StateHandlerTest {
+
+ private final static long SNAPSHOT_INTERVAL = TimeUnit.SECONDS.toMillis(300);
+ private final static long META_GENERATION = 69;
+ private final TestDriver driver;
+ private final StateMonitor monitor;
+ private final Metric metric;
+ private volatile long currentTimeMillis = 0;
+
+ public StateHandlerTest() {
+ driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(Timer.class).toInstance(new Timer() {
+
+ @Override
+ public long currentTimeMillis() {
+ return currentTimeMillis;
+ }
+ });
+ }
+ });
+ ContainerBuilder builder = driver.newContainerBuilder();
+ builder.guiceModules().install(new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(HealthMonitorConfig.class)
+ .toInstance(new HealthMonitorConfig(new HealthMonitorConfig.Builder().snapshot_interval(
+ TimeUnit.MILLISECONDS.toSeconds(SNAPSHOT_INTERVAL))));
+ }
+ });
+ monitor = builder.guiceModules().getInstance(StateMonitor.class);
+ builder.guiceModules().install(new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(StateMonitor.class).toInstance(monitor);
+ bind(MetricConsumer.class).toProvider(MetricConsumerProviders.wrap(monitor));
+ bind(ApplicationMetadataConfig.class).toInstance(new ApplicationMetadataConfig(
+ new ApplicationMetadataConfig.Builder().generation(META_GENERATION)));
+ }
+ });
+ builder.serverBindings().bind("http://*/*", builder.getInstance(StateHandler.class));
+ driver.activateContainer(builder);
+ metric = builder.getInstance(Metric.class);
+ }
+
+ @After
+ public void closeTestDriver() {
+ assertTrue(driver.close());
+ }
+
+ @Test
+ public void testReportPriorToFirstSnapshot() throws Exception {
+ metric.add("foo", 1, null);
+ metric.set("bar", 4, null);
+ JsonNode json = requestAsJson("http://localhost/state/v1/all");
+ assertEquals(json.toString(), "up", json.get("status").get("code").asText());
+ assertFalse(json.toString(), json.get("metrics").has("values"));
+ }
+
+ @Test
+ public void testReportIncludesMetricsAfterSnapshot() throws Exception {
+ metric.add("foo", 1, null);
+ metric.set("bar", 4, null);
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ JsonNode json = requestAsJson("http://localhost/state/v1/all");
+ assertEquals(json.toString(), "up", json.get("status").get("code").asText());
+ assertEquals(json.toString(), 2, json.get("metrics").get("values").size());
+ }
+
+ /**
+ * Tests that we restart an metric when it changes type from gauge to counter or back.
+ * This may happen in practice on config reloads.
+ */
+ @Test
+ public void testMetricTypeChangeIsAllowed() {
+ String metricName = "myMetric";
+ Metric.Context metricContext = null;
+
+ {
+ // Add a count metric
+ metric.add(metricName, 1, metricContext);
+ metric.add(metricName, 2, metricContext);
+ // Change it to a gauge metric
+ metric.set(metricName, 9, metricContext);
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ MetricValue resultingMetric = monitor.snapshot().iterator().next().getValue().get(metricName);
+ assertEquals(GaugeMetric.class, resultingMetric.getClass());
+ assertEquals("Value was reset and produces the last gauge value",
+ 9.0, ((GaugeMetric) resultingMetric).getLast(), 0.000001);
+ }
+
+ {
+ // Add a gauge metric
+ metric.set(metricName, 9, metricContext);
+ // Change it to a count metric
+ metric.add(metricName, 1, metricContext);
+ metric.add(metricName, 2, metricContext);
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ MetricValue resultingMetric = monitor.snapshot().iterator().next().getValue().get(metricName);
+ assertEquals(CountMetric.class, resultingMetric.getClass());
+ assertEquals("Value was reset, and changed to add semantics giving 1+2",
+ 3, ((CountMetric) resultingMetric).getCount());
+ }
+ }
+
+ @Test
+ public void testAverageAggregationOfValues() throws Exception {
+ metric.set("bar", 4, null);
+ metric.set("bar", 5, null);
+ metric.set("bar", 7, null);
+ metric.set("bar", 2, null);
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ JsonNode json = requestAsJson("http://localhost/state/v1/all");
+ assertEquals(json.toString(), "up", json.get("status").get("code").asText());
+ assertEquals(json.toString(), 1, json.get("metrics").get("values").size());
+ assertEquals(json.toString(), 4.5,
+ json.get("metrics").get("values").get(0).get("values").get("average").asDouble(), 0.001);
+ }
+
+ @Test
+ public void testSumAggregationOfCounts() throws Exception {
+ metric.add("foo", 1, null);
+ metric.add("foo", 1, null);
+ metric.add("foo", 2, null);
+ metric.add("foo", 1, null);
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ JsonNode json = requestAsJson("http://localhost/state/v1/all");
+ assertEquals(json.toString(), "up", json.get("status").get("code").asText());
+ assertEquals(json.toString(), 1, json.get("metrics").get("values").size());
+ assertEquals(json.toString(), 5,
+ json.get("metrics").get("values").get(0).get("values").get("count").asDouble(), 0.001);
+ }
+
+ @Test
+ public void testReadabilityOfJsonReport() throws Exception {
+ metric.add("foo", 1, null);
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ assertEquals("{\n" +
+ " \"metrics\": {\n" +
+ " \"snapshot\": {\n" +
+ " \"from\": 0,\n" +
+ " \"to\": 300\n" +
+ " },\n" +
+ " \"values\": [{\n" +
+ " \"name\": \"foo\",\n" +
+ " \"values\": {\n" +
+ " \"count\": 1,\n" +
+ " \"rate\": 0.0033333333333333335\n" +
+ " }\n" +
+ " }]\n" +
+ " },\n" +
+ " \"status\": {\"code\": \"up\"},\n" +
+ " \"time\": 300000\n" +
+ "}",
+ requestAsString("http://localhost/state/v1/all"));
+
+ Metric.Context ctx = metric.createContext(Collections.singletonMap("component", "test"));
+ metric.set("bar", 2, ctx);
+ metric.set("bar", 3, ctx);
+ metric.set("bar", 4, ctx);
+ metric.set("bar", 5, ctx);
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ assertEquals("{\n" +
+ " \"metrics\": {\n" +
+ " \"snapshot\": {\n" +
+ " \"from\": 300,\n" +
+ " \"to\": 600\n" +
+ " },\n" +
+ " \"values\": [\n" +
+ " {\n" +
+ " \"name\": \"foo\",\n" +
+ " \"values\": {\n" +
+ " \"count\": 0,\n" +
+ " \"rate\": 0\n" +
+ " }\n" +
+ " },\n" +
+ " {\n" +
+ " \"dimensions\": {\"component\": \"test\"},\n" +
+ " \"name\": \"bar\",\n" +
+ " \"values\": {\n" +
+ " \"average\": 3.5,\n" +
+ " \"count\": 4,\n" +
+ " \"last\": 5,\n" +
+ " \"max\": 5,\n" +
+ " \"min\": 2,\n" +
+ " \"rate\": 0.013333333333333334\n" +
+ " }\n" +
+ " }\n" +
+ " ]\n" +
+ " },\n" +
+ " \"status\": {\"code\": \"up\"},\n" +
+ " \"time\": 600000\n" +
+ "}",
+ requestAsString("http://localhost/state/v1/all"));
+ }
+
+ @Test
+ public void testNotAggregatingCountsBeyondSnapshots() throws Exception {
+ metric.add("foo", 1, null);
+ metric.add("foo", 1, null);
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ metric.add("foo", 2, null);
+ metric.add("foo", 1, null);
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ JsonNode json = requestAsJson("http://localhost/state/v1/all");
+ assertEquals(json.toString(), "up", json.get("status").get("code").asText());
+ assertEquals(json.toString(), 1, json.get("metrics").get("values").size());
+ assertEquals(json.toString(), 3,
+ json.get("metrics").get("values").get(0).get("values").get("count").asDouble(), 0.001);
+ }
+
+ @Test
+ public void testSnapshottingTimes() throws Exception {
+ metric.add("foo", 1, null);
+ metric.set("bar", 3, null);
+ // At this time we should not have done any snapshotting
+ incrementCurrentTime(SNAPSHOT_INTERVAL - 1);
+ {
+ JsonNode json = requestAsJson("http://localhost/state/v1/all");
+ assertFalse(json.toString(), json.get("metrics").has("snapshot"));
+ }
+ // At this time first snapshot should have been generated
+ incrementCurrentTime(1);
+ {
+ JsonNode json = requestAsJson("http://localhost/state/v1/all");
+ assertTrue(json.toString(), json.get("metrics").has("snapshot"));
+ assertEquals(0.0, json.get("metrics").get("snapshot").get("from").asDouble(), 0.00001);
+ assertEquals(300.0, json.get("metrics").get("snapshot").get("to").asDouble(), 0.00001);
+ }
+ // No new snapshot at this time
+ incrementCurrentTime(SNAPSHOT_INTERVAL - 1);
+ {
+ JsonNode json = requestAsJson("http://localhost/state/v1/all");
+ assertTrue(json.toString(), json.get("metrics").has("snapshot"));
+ assertEquals(0.0, json.get("metrics").get("snapshot").get("from").asDouble(), 0.00001);
+ assertEquals(300.0, json.get("metrics").get("snapshot").get("to").asDouble(), 0.00001);
+ }
+ // A new snapshot
+ incrementCurrentTime(1);
+ {
+ JsonNode json = requestAsJson("http://localhost/state/v1/all");
+ assertTrue(json.toString(), json.get("metrics").has("snapshot"));
+ assertEquals(300.0, json.get("metrics").get("snapshot").get("from").asDouble(), 0.00001);
+ assertEquals(600.0, json.get("metrics").get("snapshot").get("to").asDouble(), 0.00001);
+ }
+ }
+
+ @Test
+ public void testFreshStartOfValuesBeyondSnapshot() throws Exception {
+ metric.set("bar", 4, null);
+ metric.set("bar", 5, null);
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ metric.set("bar", 4, null);
+ metric.set("bar", 2, null);
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ JsonNode json = requestAsJson("http://localhost/state/v1/all");
+ assertEquals(json.toString(), "up", json.get("status").get("code").asText());
+ assertEquals(json.toString(), 1, json.get("metrics").get("values").size());
+ assertEquals(json.toString(), 3,
+ json.get("metrics").get("values").get(0).get("values").get("average").asDouble(), 0.001);
+ }
+
+ @Test
+ public void snapshotsPreserveLastGaugeValue() throws Exception {
+ metric.set("bar", 4, null);
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ JsonNode json = requestAsJson("http://localhost/state/v1/all");
+ JsonNode metricValues = getFirstMetricValueNode(json);
+ assertEquals(json.toString(), 4, metricValues.get("last").asDouble(), 0.001);
+ // Use 'last' as avg/min/max when none has been set explicitly during snapshot period
+ assertEquals(json.toString(), 4, metricValues.get("average").asDouble(), 0.001);
+ assertEquals(json.toString(), 4, metricValues.get("min").asDouble(), 0.001);
+ assertEquals(json.toString(), 4, metricValues.get("max").asDouble(), 0.001);
+ // Count is tracked per period.
+ assertEquals(json.toString(), 0, metricValues.get("count").asInt());
+ }
+
+ private JsonNode getFirstMetricValueNode(JsonNode root) {
+ assertEquals(root.toString(), 1, root.get("metrics").get("values").size());
+ JsonNode metricValues = root.get("metrics").get("values").get(0).get("values");
+ assertTrue(root.toString(), metricValues.has("last"));
+ return metricValues;
+ }
+
+ @Test
+ public void gaugeSnapshotsTracksCountMinMaxAvgPerPeriod() throws Exception {
+ metric.set("bar", 10000, null); // Ensure any cross-snapshot noise is visible
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ metric.set("bar", 20, null);
+ metric.set("bar", 40, null);
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ JsonNode json = requestAsJson("http://localhost/state/v1/all");
+ JsonNode metricValues = getFirstMetricValueNode(json);
+ assertEquals(json.toString(), 40, metricValues.get("last").asDouble(), 0.001);
+ // Last snapshot had explicit values set
+ assertEquals(json.toString(), 30, metricValues.get("average").asDouble(), 0.001);
+ assertEquals(json.toString(), 20, metricValues.get("min").asDouble(), 0.001);
+ assertEquals(json.toString(), 40, metricValues.get("max").asDouble(), 0.001);
+ assertEquals(json.toString(), 2, metricValues.get("count").asInt());
+ }
+
+ @Test
+ public void testHealthAggregation() throws Exception {
+ Map<String, String> dimensions1 = new TreeMap<>();
+ dimensions1.put("port", String.valueOf(Defaults.getDefaults().vespaWebServicePort()));
+ Metric.Context context1 = metric.createContext(dimensions1);
+ Map<String, String> dimensions2 = new TreeMap<>();
+ dimensions2.put("port", "80");
+ Metric.Context context2 = metric.createContext(dimensions2);
+
+ metric.add("serverNumSuccessfulResponses", 4, context1);
+ metric.add("serverNumSuccessfulResponses", 2, context2);
+ metric.set("serverTotalSuccessfulResponseLatency", 20, context1);
+ metric.set("serverTotalSuccessfulResponseLatency", 40, context2);
+ metric.add("random", 3, context1);
+ incrementCurrentTime(SNAPSHOT_INTERVAL);
+ JsonNode json = requestAsJson("http://localhost/state/v1/health");
+ assertEquals(json.toString(), "up", json.get("status").get("code").asText());
+ assertEquals(json.toString(), 2, json.get("metrics").get("values").size());
+ assertEquals(json.toString(), "requestsPerSecond",
+ json.get("metrics").get("values").get(0).get("name").asText());
+ assertEquals(json.toString(), 6,
+ json.get("metrics").get("values").get(0).get("values").get("count").asDouble(), 0.001);
+ assertEquals(json.toString(), "latencySeconds",
+ json.get("metrics").get("values").get(1).get("name").asText());
+ assertEquals(json.toString(), 0.03,
+ json.get("metrics").get("values").get(1).get("values").get("average").asDouble(), 0.001);
+ }
+
+ @Test
+ public void testStateConfig() throws Exception {
+ JsonNode root = requestAsJson("http://localhost/state/v1/config");
+
+ JsonNode config = root.get("config");
+ JsonNode container = config.get("container");
+ assertEquals(META_GENERATION, container.get("generation").asLong());
+ }
+
+ private void incrementCurrentTime(long val) {
+ currentTimeMillis += val;
+ monitor.checkTime();
+ }
+
+ private String requestAsString(String requestUri) throws Exception {
+ final BufferedContentChannel content = new BufferedContentChannel();
+ Response response = driver.dispatchRequest(requestUri, new ResponseHandler() {
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ return content;
+ }
+ }).get(60, TimeUnit.SECONDS);
+ assertNotNull(response);
+ assertEquals(Response.Status.OK, response.getStatus());
+ StringBuilder str = new StringBuilder();
+ Reader in = new InputStreamReader(content.toStream(), StandardCharsets.UTF_8);
+ for (int c; (c = in.read()) != -1; ) {
+ str.append((char)c);
+ }
+ return str.toString();
+ }
+
+ private JsonNode requestAsJson(String requestUri) throws Exception {
+ ObjectMapper mapper = new ObjectMapper();
+ return mapper.readTree(mapper.getFactory().createParser(requestAsString(requestUri)));
+ }
+}
diff --git a/container-core/src/test/java/com/yahoo/container/jdisc/state/StateMonitorBenchmarkTest.java b/container-core/src/test/java/com/yahoo/container/jdisc/state/StateMonitorBenchmarkTest.java
new file mode 100644
index 00000000000..103d22afe6d
--- /dev/null
+++ b/container-core/src/test/java/com/yahoo/container/jdisc/state/StateMonitorBenchmarkTest.java
@@ -0,0 +1,80 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.jdisc.state;
+
+import com.google.inject.Provider;
+import com.yahoo.container.jdisc.config.HealthMonitorConfig;
+import com.yahoo.jdisc.Metric;
+import com.yahoo.jdisc.application.ContainerThread;
+import com.yahoo.jdisc.application.MetricConsumer;
+import com.yahoo.jdisc.application.MetricProvider;
+import com.yahoo.jdisc.core.SystemTimer;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class StateMonitorBenchmarkTest {
+
+ private final static int NUM_THREADS = 32;
+ private final static int NUM_UPDATES = 1000;//0000;
+
+ @Test
+ public void requireThatHealthMonitorDoesNotBlockMetricThreads() throws Exception {
+ StateMonitor monitor = new StateMonitor(new HealthMonitorConfig(new HealthMonitorConfig.Builder()),
+ new SystemTimer());
+ Provider<MetricConsumer> provider = MetricConsumerProviders.wrap(monitor);
+ performUpdates(provider, 8);
+ for (int i = 1; i <= NUM_THREADS; i *= 2) {
+ long millis = performUpdates(provider, i);
+ System.err.format("%2d threads, %5d millis => %9d ups\n",
+ i, millis, (int)((i * NUM_UPDATES) / (millis / 1000.0)));
+ }
+ monitor.deconstruct();
+ }
+
+ private long performUpdates(Provider<MetricConsumer> metricProvider, int numThreads) throws Exception {
+ ThreadFactory threadFactory = new ContainerThread.Factory(metricProvider);
+ ExecutorService executor = Executors.newFixedThreadPool(numThreads, threadFactory);
+ List<Callable<Boolean>> tasks = new ArrayList<>(numThreads);
+ for (int i = 0; i < numThreads; ++i) {
+ tasks.add(new UpdateTask(new MetricProvider(metricProvider).get()));
+ }
+ long before = System.nanoTime();
+ List<Future<Boolean>> results = executor.invokeAll(tasks);
+ long after = System.nanoTime();
+ for (Future<Boolean> result : results) {
+ assertTrue(result.get());
+ }
+ return TimeUnit.NANOSECONDS.toMillis(after - before);
+ }
+
+ public static class UpdateTask implements Callable<Boolean> {
+
+ final Metric metric;
+
+ UpdateTask(Metric metric) {
+ this.metric = metric;
+ }
+
+ @Override
+ public Boolean call() throws Exception {
+ Metric.Context ctx = metric.createContext(Collections.<String, Object>emptyMap());
+ for (int i = 0; i < NUM_UPDATES; ++i) {
+ metric.add("foo", 69L, ctx);
+ }
+ return true;
+ }
+ }
+}