You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@asterixdb.apache.org by mb...@apache.org on 2017/08/27 22:46:18 UTC

asterixdb git commit: [NO ISSUE][TEST][CLUS] Update cluster state on shutdown, testfwk looping

Repository: asterixdb
Updated Branches:
  refs/heads/master 71589e574 -> 016603909


[NO ISSUE][TEST][CLUS] Update cluster state on shutdown, testfwk looping

- Set ClusterState to SHUTTING_DOWN when the cluster is shutting down
- Add ability to TestExecutor to loop over test files based on iteration count
  or duration

Change-Id: I2bd67cb92a60d76eabe1e7649d16193dd72615dd
Reviewed-on: https://asterix-gerrit.ics.uci.edu/1978
Integration-Tests: Jenkins <je...@fulliautomatix.ics.uci.edu>
Tested-by: Jenkins <je...@fulliautomatix.ics.uci.edu>
Reviewed-by: Murtadha Hubail <mh...@apache.org>


Project: http://git-wip-us.apache.org/repos/asf/asterixdb/repo
Commit: http://git-wip-us.apache.org/repos/asf/asterixdb/commit/01660390
Tree: http://git-wip-us.apache.org/repos/asf/asterixdb/tree/01660390
Diff: http://git-wip-us.apache.org/repos/asf/asterixdb/diff/01660390

Branch: refs/heads/master
Commit: 016603909756fef20f4504d4645adea60a9467e4
Parents: 71589e5
Author: Michael Blow <mb...@apache.org>
Authored: Sun Aug 27 16:03:57 2017 -0400
Committer: Michael Blow <mb...@apache.org>
Committed: Sun Aug 27 15:45:42 2017 -0700

----------------------------------------------------------------------
 .../asterix/api/http/server/Duration.java       | 236 -------------------
 .../api/http/server/NCQueryServiceServlet.java  |   1 +
 .../api/http/server/QueryServiceServlet.java    |   1 +
 .../api/http/server/ShutdownApiServlet.java     |   3 +
 .../hyracks/bootstrap/CCApplication.java        |   8 +-
 .../asterix/runtime/ParseDurationTest.java      |   2 +-
 .../asterix/test/common/TestExecutor.java       | 121 +++++++++-
 .../api/diagnostics_1/diagnostics_1.2.loop.cmd  |  21 ++
 .../org/apache/asterix/common/api/Duration.java | 236 +++++++++++++++++++
 .../common/api/IClusterManagementWork.java      |   3 +-
 10 files changed, 389 insertions(+), 243 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/asterixdb/blob/01660390/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/Duration.java
----------------------------------------------------------------------
diff --git a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/Duration.java b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/Duration.java
deleted file mode 100644
index bdda750..0000000
--- a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/Duration.java
+++ /dev/null
@@ -1,236 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.asterix.api.http.server;
-
-import org.apache.asterix.common.exceptions.ErrorCode;
-import org.apache.asterix.common.exceptions.RuntimeDataException;
-import org.apache.commons.lang3.tuple.Pair;
-import org.apache.commons.lang3.tuple.Triple;
-import org.apache.hyracks.api.exceptions.HyracksDataException;
-
-public enum Duration {
-    SEC("s", 9),
-    MILLI("ms", 6),
-    MICRO("µs", 3),
-    NANO("ns", 0);
-
-    static final long NANOSECONDS = 1;
-    static final long MICROSECONDS = 1000 * NANOSECONDS;
-    static final long MILLISECONDS = 1000 * MICROSECONDS;
-    static final long SECONDS = 1000 * MILLISECONDS;
-    static final long MINUTES = 60 * SECONDS;
-    static final long HOURS = 60 * MINUTES;
-
-    String unit;
-    int nanoDigits;
-
-    Duration(String unit, int nanoDigits) {
-        this.unit = unit;
-        this.nanoDigits = nanoDigits;
-    }
-
-    public static String formatNanos(long nanoTime) {
-        final String strTime = String.valueOf(nanoTime);
-        final int len = strTime.length();
-        for (Duration tu : Duration.values()) {
-            if (len > tu.nanoDigits) {
-                final String integer = strTime.substring(0, len - tu.nanoDigits);
-                final String fractional = strTime.substring(len - tu.nanoDigits);
-                return integer + (fractional.length() > 0 ? "." + fractional : "") + tu.unit;
-            }
-        }
-        return "illegal string value: " + strTime;
-    }
-
-    // ParseDuration parses a duration string.
-    // A duration string is a possibly signed sequence of
-    // decimal numbers, each with optional fraction and a unit suffix,
-    // such as "300ms", "-1.5h" or "2h45m".
-    // Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
-    // returns the duration in nano seconds
-    public static long parseDurationStringToNanos(String orig) throws HyracksDataException {
-        // [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+
-        String s = orig;
-        long d = 0;
-        boolean neg = false;
-        char c;
-        // Consume [-+]?
-        if (!s.isEmpty()) {
-            c = s.charAt(0);
-            if (c == '-' || c == '+') {
-                neg = c == '-';
-                s = s.substring(1);
-            }
-        }
-
-        // Special case: if all that is left is "0", this is zero.
-        if ("0".equals(s)) {
-            return 0L;
-        }
-
-        if (s.isEmpty()) {
-            throw new RuntimeDataException(ErrorCode.INVALID_DURATION, orig);
-        }
-
-        while (!s.isEmpty()) {
-            long v = 0L; // integers before decimal
-            long f = 0L; // integers after decimal
-            double scale = 1.0; // value = v + f/scale
-            // The next character must be [0-9.]
-            if (!(s.charAt(0) == '.' || '0' <= s.charAt(0) && s.charAt(0) <= '9')) {
-                throw new RuntimeDataException(ErrorCode.INVALID_DURATION, orig);
-            }
-            // Consume [0-9]*
-            int pl = s.length();
-            Pair<Long, String> pair = leadingInt(s);
-            v = pair.getLeft();
-            s = pair.getRight();
-            boolean pre = pl != s.length(); // whether we consumed anything before a period
-
-            // Consume (\.[0-9]*)?
-            boolean post = false;
-            if (!s.isEmpty() && s.charAt(0) == '.') {
-                s = s.substring(1);
-                pl = s.length();
-                Triple<Long, Double, String> triple = leadingFraction(s);
-                f = triple.getLeft();
-                scale = triple.getMiddle();
-                s = triple.getRight();
-                post = pl != s.length();
-            }
-            if (!pre && !post) {
-                // no digits (e.g. ".s" or "-.s")
-                throw new RuntimeDataException(ErrorCode.INVALID_DURATION, orig);
-            }
-
-            // Consume unit.
-            int i = 0;
-            for (; i < s.length(); i++) {
-                c = s.charAt(i);
-                if (c == '.' || '0' <= c && c <= '9') {
-                    break;
-                }
-            }
-            if (i == 0) {
-                throw new RuntimeDataException(ErrorCode.INVALID_DURATION, orig);
-            }
-            String u = s.substring(0, i);
-            s = s.substring(i);
-            long unit = getUnit(u);
-            if (v > Long.MAX_VALUE / unit) {
-                // overflow
-                throw new RuntimeDataException(ErrorCode.INVALID_DURATION, orig);
-            }
-            v *= unit;
-            if (f > 0) {
-                // float64 is needed to be nanosecond accurate for fractions of hours.
-                // v >= 0 && (f*unit/scale) <= 3.6e+12 (ns/h, h is the largest unit)
-                v += (long) (((double) f * (double) unit) / scale);
-                if (v < 0) {
-                    // overflow
-                    throw new RuntimeDataException(ErrorCode.INVALID_DURATION, orig);
-                }
-            }
-            d += v;
-            if (d < 0) {
-                // overflow
-                throw new RuntimeDataException(ErrorCode.INVALID_DURATION, orig);
-            }
-        }
-
-        if (neg) {
-            d = -d;
-        }
-        return d;
-    }
-
-    private static final long getUnit(String unit) throws HyracksDataException {
-        switch (unit) {
-            case "ns":
-                return NANOSECONDS;
-            case "us":
-            case "µs":// U+00B5 = micro symbol
-            case "μs":// U+03BC = Greek letter mu
-                return MICROSECONDS;
-            case "ms":
-                return MILLISECONDS;
-            case "s":
-                return SECONDS;
-            case "m":
-                return MINUTES;
-            case "h":
-                return HOURS;
-            default:
-                throw new RuntimeDataException(ErrorCode.UNKNOWN_DURATION_UNIT, unit);
-        }
-    }
-
-    // leadingInt consumes the leading [0-9]* from s.
-    static Pair<Long, String> leadingInt(String origin) throws HyracksDataException {
-        String s = origin;
-        long x = 0L;
-        int i = 0;
-        for (; i < s.length(); i++) {
-            char c = s.charAt(i);
-            if (c < '0' || c > '9') {
-                break;
-            }
-            if (x > Long.MAX_VALUE / 10) {
-                throw new RuntimeDataException(ErrorCode.INVALID_DURATION, origin);
-            }
-            x = x * 10 + Character.getNumericValue(c);
-            if (x < 0) {
-                throw new RuntimeDataException(ErrorCode.INVALID_DURATION, origin);
-            }
-        }
-        return Pair.of(x, s.substring(i));
-    }
-
-    // leadingFraction consumes the leading [0-9]* from s.
-    // It is used only for fractions, so does not return an error on overflow,
-    // it just stops accumulating precision.
-    static Triple<Long, Double, String> leadingFraction(String s) {
-        int i = 0;
-        long x = 0L;
-        double scale = 1.0;
-        boolean overflow = false;
-        for (; i < s.length(); i++) {
-            char c = s.charAt(i);
-            if (c < '0' || c > '9') {
-                break;
-            }
-            if (overflow) {
-                continue;
-            }
-            if (x > (1 << 63 - 1) / 10) {
-                // It's possible for overflow to give a positive number, so take care.
-                overflow = true;
-                continue;
-            }
-            long y = x * 10 + Character.getNumericValue(c);
-            if (y < 0) {
-                overflow = true;
-                continue;
-            }
-            x = y;
-            scale *= 10;
-        }
-        return Triple.of(x, scale, s.substring(i));
-    }
-}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/asterixdb/blob/01660390/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/NCQueryServiceServlet.java
----------------------------------------------------------------------
diff --git a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/NCQueryServiceServlet.java b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/NCQueryServiceServlet.java
index 69e995b..8d355ef 100644
--- a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/NCQueryServiceServlet.java
+++ b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/NCQueryServiceServlet.java
@@ -29,6 +29,7 @@ import org.apache.asterix.app.message.CancelQueryRequest;
 import org.apache.asterix.app.message.ExecuteStatementRequestMessage;
 import org.apache.asterix.app.message.ExecuteStatementResponseMessage;
 import org.apache.asterix.app.result.ResultReader;
+import org.apache.asterix.common.api.Duration;
 import org.apache.asterix.common.api.IApplicationContext;
 import org.apache.asterix.common.config.GlobalConfig;
 import org.apache.asterix.common.exceptions.ErrorCode;

http://git-wip-us.apache.org/repos/asf/asterixdb/blob/01660390/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/QueryServiceServlet.java
----------------------------------------------------------------------
diff --git a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/QueryServiceServlet.java b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/QueryServiceServlet.java
index c630636..fa67190 100644
--- a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/QueryServiceServlet.java
+++ b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/QueryServiceServlet.java
@@ -27,6 +27,7 @@ import java.util.logging.Level;
 import java.util.logging.Logger;
 
 import org.apache.asterix.algebra.base.ILangExtension;
+import org.apache.asterix.common.api.Duration;
 import org.apache.asterix.common.api.IApplicationContext;
 import org.apache.asterix.common.api.IClusterManagementWork;
 import org.apache.asterix.common.config.GlobalConfig;

http://git-wip-us.apache.org/repos/asf/asterixdb/blob/01660390/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/ShutdownApiServlet.java
----------------------------------------------------------------------
diff --git a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/ShutdownApiServlet.java b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/ShutdownApiServlet.java
index 06e2383..38f6691 100644
--- a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/ShutdownApiServlet.java
+++ b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/http/server/ShutdownApiServlet.java
@@ -19,6 +19,7 @@
 package org.apache.asterix.api.http.server;
 
 import static org.apache.asterix.api.http.server.ServletConstants.HYRACKS_CONNECTION_ATTR;
+import static org.apache.asterix.common.api.IClusterManagementWork.ClusterState.SHUTTING_DOWN;
 
 import java.io.IOException;
 import java.io.PrintWriter;
@@ -92,6 +93,8 @@ public class ShutdownApiServlet extends AbstractServlet {
             jsonObject.set("cluster", clusterState);
             final PrintWriter writer = response.writer();
             writer.print(JSONUtil.convertNode(jsonObject));
+            // accept no further queries once this servlet returns
+            ClusterStateManager.INSTANCE.setState(SHUTTING_DOWN);
             writer.close();
         } catch (Exception e) {
             GlobalConfig.ASTERIX_LOGGER.log(Level.INFO, "Exception writing response", e);

http://git-wip-us.apache.org/repos/asf/asterixdb/blob/01660390/asterixdb/asterix-app/src/main/java/org/apache/asterix/hyracks/bootstrap/CCApplication.java
----------------------------------------------------------------------
diff --git a/asterixdb/asterix-app/src/main/java/org/apache/asterix/hyracks/bootstrap/CCApplication.java b/asterixdb/asterix-app/src/main/java/org/apache/asterix/hyracks/bootstrap/CCApplication.java
index b08c1e2..6d817b8 100644
--- a/asterixdb/asterix-app/src/main/java/org/apache/asterix/hyracks/bootstrap/CCApplication.java
+++ b/asterixdb/asterix-app/src/main/java/org/apache/asterix/hyracks/bootstrap/CCApplication.java
@@ -23,6 +23,7 @@ import static org.apache.asterix.algebra.base.ILangExtension.Language.AQL;
 import static org.apache.asterix.algebra.base.ILangExtension.Language.SQLPP;
 import static org.apache.asterix.api.http.server.ServletConstants.ASTERIX_APP_CONTEXT_INFO_ATTR;
 import static org.apache.asterix.api.http.server.ServletConstants.HYRACKS_CONNECTION_ATTR;
+import static org.apache.asterix.common.api.IClusterManagementWork.ClusterState.SHUTTING_DOWN;
 
 import java.util.Arrays;
 import java.util.List;
@@ -187,12 +188,13 @@ public class CCApplication extends BaseCCApplication {
 
     @Override
     public void stop() throws Exception {
-        if (appCtx != null) {
-            ((ActiveNotificationHandler) appCtx.getActiveNotificationHandler()).stop();
-        }
         if (LOGGER.isLoggable(Level.INFO)) {
             LOGGER.info("Stopping Asterix cluster controller");
         }
+        ClusterStateManager.INSTANCE.setState(SHUTTING_DOWN);
+        if (appCtx != null) {
+            ((ActiveNotificationHandler) appCtx.getActiveNotificationHandler()).stop();
+        }
         AsterixStateProxy.unregisterRemoteObject();
         webManager.stop();
     }

http://git-wip-us.apache.org/repos/asf/asterixdb/blob/01660390/asterixdb/asterix-app/src/test/java/org/apache/asterix/runtime/ParseDurationTest.java
----------------------------------------------------------------------
diff --git a/asterixdb/asterix-app/src/test/java/org/apache/asterix/runtime/ParseDurationTest.java b/asterixdb/asterix-app/src/test/java/org/apache/asterix/runtime/ParseDurationTest.java
index 12c61d6..f2fb580 100644
--- a/asterixdb/asterix-app/src/test/java/org/apache/asterix/runtime/ParseDurationTest.java
+++ b/asterixdb/asterix-app/src/test/java/org/apache/asterix/runtime/ParseDurationTest.java
@@ -18,7 +18,7 @@
  */
 package org.apache.asterix.runtime;
 
-import org.apache.asterix.api.http.server.Duration;
+import org.apache.asterix.common.api.Duration;
 import org.apache.hyracks.api.exceptions.HyracksDataException;
 import org.junit.Assert;
 import org.junit.Test;

http://git-wip-us.apache.org/repos/asf/asterixdb/blob/01660390/asterixdb/asterix-app/src/test/java/org/apache/asterix/test/common/TestExecutor.java
----------------------------------------------------------------------
diff --git a/asterixdb/asterix-app/src/test/java/org/apache/asterix/test/common/TestExecutor.java b/asterixdb/asterix-app/src/test/java/org/apache/asterix/test/common/TestExecutor.java
index e07ec72..cfd01ac 100644
--- a/asterixdb/asterix-app/src/test/java/org/apache/asterix/test/common/TestExecutor.java
+++ b/asterixdb/asterix-app/src/test/java/org/apache/asterix/test/common/TestExecutor.java
@@ -43,6 +43,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.ListIterator;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ExecutorService;
@@ -56,6 +57,7 @@ import java.util.logging.Logger;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import org.apache.asterix.common.api.Duration;
 import org.apache.asterix.common.config.GlobalConfig;
 import org.apache.asterix.common.utils.Servlets;
 import org.apache.asterix.test.server.ITestServer;
@@ -131,6 +133,7 @@ public class TestExecutor {
     protected final List<InetSocketAddress> endpoints;
     protected int endpointSelector;
     protected ITestLibrarian librarian;
+    private Map<File, TestLoop> testLoops = new HashMap<>();
 
     public TestExecutor() {
         this(Inet4Address.getLoopbackAddress().getHostAddress(), 19002);
@@ -1080,6 +1083,59 @@ public class TestExecutor {
                     killNC(nodeId, cUnit);
                 }
                 break;
+            case "loop":
+                TestLoop testLoop = testLoops.get(testFile);
+                if (testLoop == null) {
+                    lines = stripAllComments(statement).trim().split("\n");
+                    String target = null;
+                    int count = -1;
+                    long durationSecs = -1;
+                    for (String line : lines) {
+                        command = line.trim().split(" ");
+                        switch (command[0]) {
+                            case "target":
+                                if (target != null) {
+                                    throw new IllegalStateException("duplicate target");
+                                }
+                                target = command[1];
+                                break;
+                            case "count":
+                                if (count != -1) {
+                                    throw new IllegalStateException("duplicate count");
+                                }
+                                count = Integer.parseInt(command[1]);
+                                break;
+                            case "duration":
+                                if (durationSecs != -1) {
+                                    throw new IllegalStateException("duplicate duration");
+                                }
+                                long duration = Duration.parseDurationStringToNanos(command[1]);
+                                durationSecs = TimeUnit.NANOSECONDS.toSeconds(duration);
+                                if (durationSecs < 1) {
+                                    throw new IllegalArgumentException("duration cannot be shorter than 1s");
+                                } else if (TimeUnit.SECONDS.toDays(durationSecs) > 1) {
+                                    throw new IllegalArgumentException("duration cannot be exceed 1d");
+                                }
+                                break;
+                            default:
+                                throw new IllegalArgumentException("unknown directive: " + command[0]);
+                        }
+                    }
+                    if (target == null || (count == -1 && durationSecs == -1) || (count != -1 && durationSecs != -1)) {
+                        throw new IllegalStateException("Must specify 'target' and exactly one of 'count', 'duration'");
+                    }
+                    if (count != -1) {
+                        testLoop = TestLoop.createLoop(target, count);
+                    } else {
+                        testLoop = TestLoop.createLoop(target, durationSecs, TimeUnit.SECONDS);
+                    }
+                    testLoops.put(testFile, testLoop);
+                }
+                testLoop.executeLoop();
+                // we only reach here if the loop is over
+                testLoops.remove(testFile);
+                break;
+
             default:
                 throw new IllegalArgumentException("No statements of type " + ctx.getType());
         }
@@ -1089,7 +1145,7 @@ public class TestExecutor {
             String reqType, File testFile, File expectedResultFile, File actualResultFile, MutableInt queryCount,
             int numResultFiles, String extension, ComparisonEnum compare) throws Exception {
         String handleVar = getHandleVariable(statement);
-        final String trimmedPathAndQuery = stripLineComments(stripJavaComments(statement)).trim();
+        final String trimmedPathAndQuery = stripAllComments(statement).trim();
         final String variablesReplaced = replaceVarRef(trimmedPathAndQuery, variableCtx);
         final List<Parameter> params = extractParameters(statement);
         final Predicate<Integer> statusCodePredicate = extractStatusCodePredicate(statement);
@@ -1361,13 +1417,26 @@ public class TestExecutor {
             Map<String, Object> variableCtx = new HashMap<>();
             List<TestFileContext> testFileCtxs = testCaseCtx.getTestFiles(cUnit);
             List<TestFileContext> expectedResultFileCtxs = testCaseCtx.getExpectedResultFiles(cUnit);
-            for (TestFileContext ctx : testFileCtxs) {
+            int[] savedQueryCounts = new int[numOfFiles + testFileCtxs.size()];
+            for (ListIterator<TestFileContext> iter = testFileCtxs.listIterator(); iter.hasNext();) {
+                TestFileContext ctx = iter.next();
+                savedQueryCounts[numOfFiles] = queryCount.getValue();
                 numOfFiles++;
                 final File testFile = ctx.getFile();
                 final String statement = readTestFile(testFile);
                 try {
                     executeTestFile(testCaseCtx, ctx, variableCtx, statement, isDmlRecoveryTest, pb, cUnit, queryCount,
                             expectedResultFileCtxs, testFile, actualPath);
+                } catch (TestLoop loop) {
+                    // rewind the iterator until we find our target
+                    while (!ctx.getFile().getName().equals(loop.getTarget())) {
+                        if (!iter.hasPrevious()) {
+                            throw new IllegalStateException("unable to find loop target '" + loop.getTarget() + "'!");
+                        }
+                        ctx = iter.previous();
+                        numOfFiles--;
+                        queryCount.setValue(savedQueryCounts[numOfFiles]);
+                    }
                 } catch (Exception e) {
                     System.err.println("testFile " + testFile.toString() + " raised an exception: " + e);
                     numOfErrors++;
@@ -1442,6 +1511,10 @@ public class TestExecutor {
         return JAVA_LINE_COMMENT_PATTERN.matcher(s).replaceAll("");
     }
 
+    public static String stripAllComments(String statement) {
+        return stripLineComments(stripJavaComments(statement));
+    }
+
     public void cleanup(String testCase, List<String> badtestcases) throws Exception {
         try {
             ArrayList<String> toBeDropped = new ArrayList<>();
@@ -1531,4 +1604,48 @@ public class TestExecutor {
         LOGGER.info("Cluster state now " + desiredState);
     }
 
+    abstract static class TestLoop extends Exception {
+
+        private final String target;
+
+        TestLoop(String target) {
+            this.target = target;
+        }
+
+        static TestLoop createLoop(String target, final int count) {
+            LOGGER.info("Starting loop '" + count + " times back to '" + target + "'...");
+            return new TestLoop(target) {
+                int remainingLoops = count;
+
+                @Override
+                void executeLoop() throws TestLoop {
+                    if (remainingLoops-- > 0) {
+                        throw this;
+                    }
+                    LOGGER.info("Loop to '" + target + "' complete!");
+                }
+            };
+        }
+
+        static TestLoop createLoop(String target, long duration, TimeUnit unit) {
+            LOGGER.info("Starting loop for " + unit.toSeconds(duration) + "s back to '" + target + "'...");
+            return new TestLoop(target) {
+                long endTime = unit.toMillis(duration) + System.currentTimeMillis();
+
+                @Override
+                void executeLoop() throws TestLoop {
+                    if (System.currentTimeMillis() < endTime) {
+                        throw this;
+                    }
+                    LOGGER.info("Loop to '" + target + "' complete!");
+                }
+            };
+        }
+
+        abstract void executeLoop() throws TestLoop;
+
+        public String getTarget() {
+            return target;
+        }
+    }
 }

http://git-wip-us.apache.org/repos/asf/asterixdb/blob/01660390/asterixdb/asterix-app/src/test/resources/runtimets/queries/api/diagnostics_1/diagnostics_1.2.loop.cmd
----------------------------------------------------------------------
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries/api/diagnostics_1/diagnostics_1.2.loop.cmd b/asterixdb/asterix-app/src/test/resources/runtimets/queries/api/diagnostics_1/diagnostics_1.2.loop.cmd
new file mode 100644
index 0000000..2660b46
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries/api/diagnostics_1/diagnostics_1.2.loop.cmd
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+target diagnostics_1.1.get.http
+count 2

http://git-wip-us.apache.org/repos/asf/asterixdb/blob/01660390/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/api/Duration.java
----------------------------------------------------------------------
diff --git a/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/api/Duration.java b/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/api/Duration.java
new file mode 100644
index 0000000..4338222
--- /dev/null
+++ b/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/api/Duration.java
@@ -0,0 +1,236 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.asterix.common.api;
+
+import org.apache.asterix.common.exceptions.ErrorCode;
+import org.apache.asterix.common.exceptions.RuntimeDataException;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.commons.lang3.tuple.Triple;
+import org.apache.hyracks.api.exceptions.HyracksDataException;
+
+public enum Duration {
+    SEC("s", 9),
+    MILLI("ms", 6),
+    MICRO("µs", 3),
+    NANO("ns", 0);
+
+    static final long NANOSECONDS = 1;
+    static final long MICROSECONDS = 1000 * NANOSECONDS;
+    static final long MILLISECONDS = 1000 * MICROSECONDS;
+    static final long SECONDS = 1000 * MILLISECONDS;
+    static final long MINUTES = 60 * SECONDS;
+    static final long HOURS = 60 * MINUTES;
+
+    String unit;
+    int nanoDigits;
+
+    Duration(String unit, int nanoDigits) {
+        this.unit = unit;
+        this.nanoDigits = nanoDigits;
+    }
+
+    public static String formatNanos(long nanoTime) {
+        final String strTime = String.valueOf(nanoTime);
+        final int len = strTime.length();
+        for (Duration tu : Duration.values()) {
+            if (len > tu.nanoDigits) {
+                final String integer = strTime.substring(0, len - tu.nanoDigits);
+                final String fractional = strTime.substring(len - tu.nanoDigits);
+                return integer + (fractional.length() > 0 ? "." + fractional : "") + tu.unit;
+            }
+        }
+        return "illegal string value: " + strTime;
+    }
+
+    // ParseDuration parses a duration string.
+    // A duration string is a possibly signed sequence of
+    // decimal numbers, each with optional fraction and a unit suffix,
+    // such as "300ms", "-1.5h" or "2h45m".
+    // Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
+    // returns the duration in nano seconds
+    public static long parseDurationStringToNanos(String orig) throws HyracksDataException {
+        // [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+
+        String s = orig;
+        long d = 0;
+        boolean neg = false;
+        char c;
+        // Consume [-+]?
+        if (!s.isEmpty()) {
+            c = s.charAt(0);
+            if (c == '-' || c == '+') {
+                neg = c == '-';
+                s = s.substring(1);
+            }
+        }
+
+        // Special case: if all that is left is "0", this is zero.
+        if ("0".equals(s)) {
+            return 0L;
+        }
+
+        if (s.isEmpty()) {
+            throw new RuntimeDataException(ErrorCode.INVALID_DURATION, orig);
+        }
+
+        while (!s.isEmpty()) {
+            long v = 0L; // integers before decimal
+            long f = 0L; // integers after decimal
+            double scale = 1.0; // value = v + f/scale
+            // The next character must be [0-9.]
+            if (!(s.charAt(0) == '.' || '0' <= s.charAt(0) && s.charAt(0) <= '9')) {
+                throw new RuntimeDataException(ErrorCode.INVALID_DURATION, orig);
+            }
+            // Consume [0-9]*
+            int pl = s.length();
+            Pair<Long, String> pair = leadingInt(s);
+            v = pair.getLeft();
+            s = pair.getRight();
+            boolean pre = pl != s.length(); // whether we consumed anything before a period
+
+            // Consume (\.[0-9]*)?
+            boolean post = false;
+            if (!s.isEmpty() && s.charAt(0) == '.') {
+                s = s.substring(1);
+                pl = s.length();
+                Triple<Long, Double, String> triple = leadingFraction(s);
+                f = triple.getLeft();
+                scale = triple.getMiddle();
+                s = triple.getRight();
+                post = pl != s.length();
+            }
+            if (!pre && !post) {
+                // no digits (e.g. ".s" or "-.s")
+                throw new RuntimeDataException(ErrorCode.INVALID_DURATION, orig);
+            }
+
+            // Consume unit.
+            int i = 0;
+            for (; i < s.length(); i++) {
+                c = s.charAt(i);
+                if (c == '.' || '0' <= c && c <= '9') {
+                    break;
+                }
+            }
+            if (i == 0) {
+                throw new RuntimeDataException(ErrorCode.INVALID_DURATION, orig);
+            }
+            String u = s.substring(0, i);
+            s = s.substring(i);
+            long unit = getUnit(u);
+            if (v > Long.MAX_VALUE / unit) {
+                // overflow
+                throw new RuntimeDataException(ErrorCode.INVALID_DURATION, orig);
+            }
+            v *= unit;
+            if (f > 0) {
+                // float64 is needed to be nanosecond accurate for fractions of hours.
+                // v >= 0 && (f*unit/scale) <= 3.6e+12 (ns/h, h is the largest unit)
+                v += (long) (((double) f * (double) unit) / scale);
+                if (v < 0) {
+                    // overflow
+                    throw new RuntimeDataException(ErrorCode.INVALID_DURATION, orig);
+                }
+            }
+            d += v;
+            if (d < 0) {
+                // overflow
+                throw new RuntimeDataException(ErrorCode.INVALID_DURATION, orig);
+            }
+        }
+
+        if (neg) {
+            d = -d;
+        }
+        return d;
+    }
+
+    private static final long getUnit(String unit) throws HyracksDataException {
+        switch (unit) {
+            case "ns":
+                return NANOSECONDS;
+            case "us":
+            case "µs":// U+00B5 = micro symbol
+            case "μs":// U+03BC = Greek letter mu
+                return MICROSECONDS;
+            case "ms":
+                return MILLISECONDS;
+            case "s":
+                return SECONDS;
+            case "m":
+                return MINUTES;
+            case "h":
+                return HOURS;
+            default:
+                throw new RuntimeDataException(ErrorCode.UNKNOWN_DURATION_UNIT, unit);
+        }
+    }
+
+    // leadingInt consumes the leading [0-9]* from s.
+    static Pair<Long, String> leadingInt(String origin) throws HyracksDataException {
+        String s = origin;
+        long x = 0L;
+        int i = 0;
+        for (; i < s.length(); i++) {
+            char c = s.charAt(i);
+            if (c < '0' || c > '9') {
+                break;
+            }
+            if (x > Long.MAX_VALUE / 10) {
+                throw new RuntimeDataException(ErrorCode.INVALID_DURATION, origin);
+            }
+            x = x * 10 + Character.getNumericValue(c);
+            if (x < 0) {
+                throw new RuntimeDataException(ErrorCode.INVALID_DURATION, origin);
+            }
+        }
+        return Pair.of(x, s.substring(i));
+    }
+
+    // leadingFraction consumes the leading [0-9]* from s.
+    // It is used only for fractions, so does not return an error on overflow,
+    // it just stops accumulating precision.
+    static Triple<Long, Double, String> leadingFraction(String s) {
+        int i = 0;
+        long x = 0L;
+        double scale = 1.0;
+        boolean overflow = false;
+        for (; i < s.length(); i++) {
+            char c = s.charAt(i);
+            if (c < '0' || c > '9') {
+                break;
+            }
+            if (overflow) {
+                continue;
+            }
+            if (x > (1 << 63 - 1) / 10) {
+                // It's possible for overflow to give a positive number, so take care.
+                overflow = true;
+                continue;
+            }
+            long y = x * 10 + Character.getNumericValue(c);
+            if (y < 0) {
+                overflow = true;
+                continue;
+            }
+            x = y;
+            scale *= 10;
+        }
+        return Triple.of(x, scale, s.substring(i));
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/asterixdb/blob/01660390/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/api/IClusterManagementWork.java
----------------------------------------------------------------------
diff --git a/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/api/IClusterManagementWork.java b/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/api/IClusterManagementWork.java
index 323df65..bdbf4a5 100644
--- a/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/api/IClusterManagementWork.java
+++ b/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/api/IClusterManagementWork.java
@@ -30,7 +30,8 @@ public interface IClusterManagementWork {
         PENDING,
         ACTIVE,
         UNUSABLE,
-        REBALANCING
+        REBALANCING,
+        SHUTTING_DOWN
     }
 
     public WorkType getClusterManagementWorkType();