summaryrefslogtreecommitdiffstats
path: root/container-core/src/test/java/com/yahoo/container/jdisc/state/StateHandlerTest.java
diff options
context:
space:
mode:
Diffstat (limited to 'container-core/src/test/java/com/yahoo/container/jdisc/state/StateHandlerTest.java')
-rw-r--r--container-core/src/test/java/com/yahoo/container/jdisc/state/StateHandlerTest.java409
1 files changed, 409 insertions, 0 deletions
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)));
+ }
+}