From bf5e3d7144c9f93f3c1c86aa03fd668b4cf07c1a Mon Sep 17 00:00:00 2001
From: Mohit Kataria <tihom88@gmail.com>
Date: Mon, 13 May 2019 11:16:53 +0530
Subject: [PATCH] OAK-8309: SLOW_QUERY_COUNT don't get updated for each slow
 query

---
 .../jackrabbit/oak/query/FilterIterators.java |   2 +-
 .../query/RuntimeNodeTraversalException.java  |   8 +
 .../oak/query/ast/SelectorImpl.java           | 132 ++++++++++------
 .../oak/query/stats/QueryStatsData.java       |  27 +---
 .../oak/spi/query/SlowQueryMetricTest.java    | 142 ++++++++++++++++++
 .../index/lucene/ReopenedLuceneIndexTest.java |   5 +-
 6 files changed, 237 insertions(+), 79 deletions(-)
 create mode 100644 oak-core/src/main/java/org/apache/jackrabbit/oak/query/RuntimeNodeTraversalException.java
 create mode 100644 oak-core/src/test/java/org/apache/jackrabbit/oak/spi/query/SlowQueryMetricTest.java

diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/FilterIterators.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/FilterIterators.java
index dcd7ffee80..df20fecd8c 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/FilterIterators.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/FilterIterators.java
@@ -67,7 +67,7 @@ public class FilterIterators {
         if (count > maxReadEntries) {
             String message = "The query read or traversed more than " + 
                     maxReadEntries + " nodes.";
-            UnsupportedOperationException e = new UnsupportedOperationException(
+            RuntimeNodeTraversalException e = new RuntimeNodeTraversalException(
                     message + 
                     " To avoid affecting other tasks, processing was stopped.");
             LOG.warn(message, e);
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/RuntimeNodeTraversalException.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/RuntimeNodeTraversalException.java
new file mode 100644
index 0000000000..caf5498b2b
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/RuntimeNodeTraversalException.java
@@ -0,0 +1,8 @@
+package org.apache.jackrabbit.oak.query;
+
+public class RuntimeNodeTraversalException extends UnsupportedOperationException {
+
+    public RuntimeNodeTraversalException(String message) {
+        super(message);
+    }
+}
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SelectorImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SelectorImpl.java
index b5f2ce8410..fc3500158f 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SelectorImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/ast/SelectorImpl.java
@@ -38,8 +38,10 @@ import org.apache.jackrabbit.oak.core.ImmutableRoot;
 import org.apache.jackrabbit.oak.plugins.memory.PropertyBuilder;
 import org.apache.jackrabbit.oak.plugins.tree.TreeUtil;
 import org.apache.jackrabbit.oak.query.ExecutionContext;
+import org.apache.jackrabbit.oak.query.QueryEngineSettings;
 import org.apache.jackrabbit.oak.query.QueryImpl;
 import org.apache.jackrabbit.oak.query.QueryOptions;
+import org.apache.jackrabbit.oak.query.RuntimeNodeTraversalException;
 import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextExpression;
 import org.apache.jackrabbit.oak.query.index.FilterImpl;
 import org.apache.jackrabbit.oak.query.plan.ExecutionPlan;
@@ -56,6 +58,8 @@ import org.apache.jackrabbit.oak.spi.query.QueryIndex.IndexPlan;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
 import org.apache.jackrabbit.oak.stats.StatsOptions;
 import org.apache.jackrabbit.oak.stats.TimerStats;
+import org.apache.jackrabbit.oak.stats.CounterStats;
+import org.apache.jackrabbit.oak.stats.HistogramStats;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 import org.slf4j.Logger;
@@ -69,22 +73,22 @@ import com.google.common.collect.Iterables;
  */
 public class SelectorImpl extends SourceImpl {
     private static final Logger LOG = LoggerFactory.getLogger(SelectorImpl.class);
-    
+
     private static final Boolean TIMER_DISABLED = Boolean.getBoolean("oak.query.timerDisabled");
-    
+
     // The sample rate. Must be a power of 2.
     private static final Long TIMER_SAMPLE_RATE = Long.getLong("oak.query.timerSampleRate", 0x100);
-    
+
     private static long timerSampleCounter;
-    
+
     // TODO possibly support using multiple indexes (using index intersection / index merge)
     private SelectorExecutionPlan plan;
-    
+
     /**
      * The WHERE clause of the query.
      */
     private ConstraintImpl queryConstraint;
-    
+
     /**
      * The join condition of this selector that can be evaluated at execution
      * time. For the query "select * from nt:base as a inner join nt:base as b
@@ -122,14 +126,14 @@ public class SelectorImpl extends SourceImpl {
      * flag is set
      */
     private final Set<String> mixinTypes;
-    
+
     /**
      * Whether this selector is the parent of a descendent or parent-child join.
      * Access rights don't need to be checked in such selectors (unless there
      * are conditions on the selector).
      */
-    private boolean isParent;  
-    
+    private boolean isParent;
+
     /**
      * Whether this selector is the left hand side of a left outer join.
      * Right outer joins are converted to left outer join.
@@ -141,7 +145,7 @@ public class SelectorImpl extends SourceImpl {
      * Right outer joins are converted to left outer join.
      */
     private boolean outerJoinRightHandSide;
-    
+
     /**
      * The list of all join conditions this selector is involved. For the query
      * "select * from nt:base as a inner join nt:base as b on a.x =
@@ -161,7 +165,7 @@ public class SelectorImpl extends SourceImpl {
      * These constraints are collected during the prepare phase.
      */
     private final List<ConstraintImpl> selectorConstraints = newArrayList();
-    
+
     private Cursor cursor;
     private IndexRow currentRow;
     private int scanCount;
@@ -171,6 +175,12 @@ public class SelectorImpl extends SourceImpl {
 
     private CachedTree cachedTree;
 
+    private final long SLOW_QUERY_HISTOGRAM = 1;
+    private final long TOTAL_QUERY_HISTOGRAM = 0;
+    private final String SLOW_QUERY_PERCENTILE_METRICS_NAME = "SLOW_QUERY_PERCENTILE_METRICS";
+    private final String SLOW_QUERY_COUNT_NAME = "SLOW_QUERY_COUNT";
+    private boolean updateTotalQueryHistogram = true;
+
     public SelectorImpl(NodeTypeInfo nodeTypeInfo, String selectorName) {
         this.nodeTypeInfo = checkNotNull(nodeTypeInfo);
         this.selectorName = checkNotNull(selectorName);
@@ -210,7 +220,7 @@ public class SelectorImpl extends SourceImpl {
 
     /**
      * @return all of the matching supertypes, or empty if the
-     *         {@link #matchesAllTypes} flag is set
+     * {@link #matchesAllTypes} flag is set
      */
     @NotNull
     public Set<String> getSupertypes() {
@@ -219,7 +229,7 @@ public class SelectorImpl extends SourceImpl {
 
     /**
      * @return all of the matching primary subtypes, or empty if the
-     *         {@link #matchesAllTypes} flag is set
+     * {@link #matchesAllTypes} flag is set
      */
     @NotNull
     public Set<String> getPrimaryTypes() {
@@ -228,7 +238,7 @@ public class SelectorImpl extends SourceImpl {
 
     /**
      * @return all of the matching mixin types, or empty if the
-     *         {@link #matchesAllTypes} flag is set
+     * {@link #matchesAllTypes} flag is set
      */
     @NotNull
     public Set<String> getMixinTypes() {
@@ -252,7 +262,7 @@ public class SelectorImpl extends SourceImpl {
     public boolean isPrepared() {
         return plan != null;
     }
-    
+
     @Override
     public void unprepare() {
         plan = null;
@@ -263,7 +273,7 @@ public class SelectorImpl extends SourceImpl {
         joinCondition = null;
         allJoinConditions.clear();
     }
-    
+
     @Override
     public void prepare(ExecutionPlan p) {
         if (!(p instanceof SelectorExecutionPlan)) {
@@ -276,7 +286,7 @@ public class SelectorImpl extends SourceImpl {
         pushDown();
         this.plan = selectorPlan;
     }
-    
+
     private void pushDown() {
         if (queryConstraint != null) {
             queryConstraint.restrictPushDown(this);
@@ -297,22 +307,22 @@ public class SelectorImpl extends SourceImpl {
         plan = query.getBestSelectorExecutionPlan(createFilter(true));
         return plan;
     }
-    
+
     public SelectorExecutionPlan getExecutionPlan() {
         return plan;
     }
-    
+
     @Override
     public void setQueryConstraint(ConstraintImpl queryConstraint) {
         this.queryConstraint = queryConstraint;
-    }    
-    
+    }
+
     @Override
     public void setOuterJoin(boolean outerJoinLeftHandSide, boolean outerJoinRightHandSide) {
         this.outerJoinLeftHandSide = outerJoinLeftHandSide;
         this.outerJoinRightHandSide = outerJoinRightHandSide;
-    }    
-    
+    }
+
     @Override
     public void addJoinCondition(JoinConditionImpl joinCondition, boolean forThisSelector) {
         if (forThisSelector) {
@@ -323,7 +333,7 @@ public class SelectorImpl extends SourceImpl {
             isParent = true;
         }
     }
-    
+
     @Override
     public void execute(NodeState rootState) {
         long start = startTimer();
@@ -333,7 +343,7 @@ public class SelectorImpl extends SourceImpl {
             stopTimer(start, true);
         }
     }
-    
+
     private void executeInternal(NodeState rootState) {
         QueryIndex index = plan.getIndex();
         timerDuration = null;
@@ -354,14 +364,14 @@ public class SelectorImpl extends SourceImpl {
             cursor = index.query(f, rootState);
         }
     }
-    
+
     private long startTimer() {
         if (TIMER_DISABLED) {
             return -1;
         }
         return System.nanoTime();
     }
-    
+
     private void stopTimer(long start, boolean execute) {
         if (start == -1) {
             return;
@@ -382,7 +392,7 @@ public class SelectorImpl extends SourceImpl {
         if (t == null) {
             // reuse the timer (in the normal case)
             t = timerDuration = query.getSettings().getStatisticsProvider().
-                getTimer("QUERY_DURATION_" + planIndexName, StatsOptions.METRICS_ONLY);
+                    getTimer("QUERY_DURATION_" + planIndexName, StatsOptions.METRICS_ONLY);
         }
         t.update(timeNanos, TimeUnit.NANOSECONDS);
     }
@@ -432,8 +442,8 @@ public class SelectorImpl extends SourceImpl {
 
     /**
      * Create the filter condition for planning or execution.
-     * 
-     * @param preparing whether a filter for the prepare phase should be made 
+     *
+     * @param preparing whether a filter for the prepare phase should be made
      * @return the filter
      */
     @Override
@@ -458,7 +468,7 @@ public class SelectorImpl extends SourceImpl {
                 }
             }
         }
-        
+
         // all conditions can be pushed to the selectors -
         // except in some cases to "outer joined" selectors,
         // but the exceptions are handled in the condition
@@ -479,11 +489,11 @@ public class SelectorImpl extends SourceImpl {
         QueryOptions options = query.getQueryOptions();
         if (options != null) {
             if (options.indexName != null) {
-                f.restrictProperty(IndexConstants.INDEX_NAME_OPTION, 
+                f.restrictProperty(IndexConstants.INDEX_NAME_OPTION,
                         Operator.EQUAL, PropertyValues.newString(options.indexName));
             }
             if (options.indexTag != null) {
-                f.restrictProperty(IndexConstants.INDEX_TAG_OPTION, 
+                f.restrictProperty(IndexConstants.INDEX_TAG_OPTION,
                         Operator.EQUAL, PropertyValues.newString(options.indexTag));
             }
         }
@@ -499,12 +509,19 @@ public class SelectorImpl extends SourceImpl {
             stopTimer(start, true);
         }
     }
-    
+
     private boolean nextInternal() {
         while (cursor != null && cursor.hasNext()) {
             scanCount++;
-            query.getQueryExecutionStats().scan(1, scanCount, query.getSettings());
-            currentRow = cursor.next();
+            query.getQueryExecutionStats().scan(1, scanCount);
+            try {
+                totalQueryStats(query.getSettings());
+                currentRow = cursor.next();
+            } catch (RuntimeNodeTraversalException e) {
+                addSlowQueryStats(query.getSettings());
+                LOG.warn(e.getMessage() + " for query " + query.getStatement());
+                throw e;
+            }
             if (isParent) {
                 // we must not check whether the _parent_ is readable
                 // for joins of type
@@ -541,6 +558,21 @@ public class SelectorImpl extends SourceImpl {
         return false;
     }
 
+    private void totalQueryStats(QueryEngineSettings queryEngineSettings) {
+        if (updateTotalQueryHistogram) {
+            updateTotalQueryHistogram = false;
+            HistogramStats histogramStats = queryEngineSettings.getStatisticsProvider().getHistogram(SLOW_QUERY_PERCENTILE_METRICS_NAME, StatsOptions.METRICS_ONLY);
+            histogramStats.update(TOTAL_QUERY_HISTOGRAM);
+        }
+    }
+
+    private void addSlowQueryStats(QueryEngineSettings queryEngineSettings) {
+        HistogramStats histogramStats = queryEngineSettings.getStatisticsProvider().getHistogram(SLOW_QUERY_PERCENTILE_METRICS_NAME, StatsOptions.METRICS_ONLY);
+        histogramStats.update(SLOW_QUERY_HISTOGRAM);
+        CounterStats slowQueryCounter = queryEngineSettings.getStatisticsProvider().getCounterStats(SLOW_QUERY_COUNT_NAME, StatsOptions.METRICS_ONLY);
+        slowQueryCounter.inc();
+    }
+
     private boolean evaluateCurrentRow() {
         if (currentRow.isVirtualRow()) {
             //null path implies that all checks are already done -- we just need to pass it through
@@ -583,7 +615,7 @@ public class SelectorImpl extends SourceImpl {
             }
         }
         // no matches found
-        return false; 
+        return false;
     }
 
     /**
@@ -594,10 +626,10 @@ public class SelectorImpl extends SourceImpl {
     public String currentPath() {
         return cursor == null ? null : currentRow.getPath();
     }
-    
+
     /**
      * Get the tree at the current path.
-     * 
+     *
      * @return the current tree, or null
      */
     @Nullable
@@ -613,15 +645,15 @@ public class SelectorImpl extends SourceImpl {
     Tree getTree(@NotNull String path) {
         return getCachedTree(path).getTree();
     }
-    
+
     /**
      * Get the tree at the given path.
-     * 
+     *
      * @param path the path
      * @return the tree, or null
      */
     @NotNull
-    private CachedTree getCachedTree(@NotNull  String path) {
+    private CachedTree getCachedTree(@NotNull String path) {
         if (cachedTree == null || !cachedTree.denotes(path)) {
             cachedTree = new CachedTree(path, query);
         }
@@ -630,7 +662,7 @@ public class SelectorImpl extends SourceImpl {
 
     /**
      * The value for the given selector for the current node.
-     * 
+     *
      * @param propertyName the JCR (not normalized) property name
      * @return the property value
      */
@@ -642,7 +674,7 @@ public class SelectorImpl extends SourceImpl {
     /**
      * The value for the given selector for the current node, filtered by
      * property type.
-     * 
+     *
      * @param propertyName the JCR (not normalized) property name
      * @param propertyType only include properties of this type
      * @return the property value (possibly null)
@@ -655,7 +687,7 @@ public class SelectorImpl extends SourceImpl {
     /**
      * Get the property value. The property name may be relative. The special
      * property names "jcr:path", "jcr:score" and "rep:excerpt" are supported.
-     * 
+     *
      * @param oakPropertyName (must already be normalized)
      * @return the property value or null if not found
      */
@@ -669,7 +701,7 @@ public class SelectorImpl extends SourceImpl {
             Tree t = currentTree();
             if (t != null) {
                 LOG.trace("currentOakProperty() - '*' case. looking for '{}' in '{}'",
-                    oakPropertyName, t.getPath());
+                        oakPropertyName, t.getPath());
             }
             ArrayList<PropertyValue> list = new ArrayList<PropertyValue>();
             readOakProperties(list, t, oakPropertyName, propertyType);
@@ -731,7 +763,7 @@ public class SelectorImpl extends SourceImpl {
         }
         return currentOakProperty(t, oakPropertyName, propertyType);
     }
-    
+
     private PropertyValue currentOakProperty(Tree t, String oakPropertyName, Integer propertyType) {
         PropertyValue result;
         if ((t == null || !t.exists()) && (currentRow == null || !currentRow.isVirtualRow())) {
@@ -769,7 +801,7 @@ public class SelectorImpl extends SourceImpl {
         }
         return result;
     }
-    
+
     private void readOakProperties(ArrayList<PropertyValue> target, Tree t, String oakPropertyName, Integer propertyType) {
         boolean skipCurrentNode = false;
 
@@ -778,7 +810,7 @@ public class SelectorImpl extends SourceImpl {
                 return;
             }
             LOG.trace("readOakProperties() - reading '{}' for '{}'", t.getPath(),
-                oakPropertyName);
+                    oakPropertyName);
             int slash = oakPropertyName.indexOf('/');
             if (slash < 0) {
                 break;
@@ -850,7 +882,7 @@ public class SelectorImpl extends SourceImpl {
         }
         return selectorName.equals(((SelectorImpl) other).selectorName);
     }
-    
+
     @Override
     public int hashCode() {
         return selectorName.hashCode();
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/stats/QueryStatsData.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/stats/QueryStatsData.java
index c3b38c1afb..389b272683 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/query/stats/QueryStatsData.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/query/stats/QueryStatsData.java
@@ -17,10 +17,6 @@
 package org.apache.jackrabbit.oak.query.stats;
 
 import org.apache.jackrabbit.oak.commons.json.JsopBuilder;
-import org.apache.jackrabbit.oak.query.QueryEngineSettings;
-import org.apache.jackrabbit.oak.stats.CounterStats;
-import org.apache.jackrabbit.oak.stats.HistogramStats;
-import org.apache.jackrabbit.oak.stats.StatsOptions;
 
 public class QueryStatsData {
     
@@ -47,8 +43,6 @@ public class QueryStatsData {
     private long readNanos;
     private long maxTimeNanos;
     private boolean captureStackTraces;
-    private boolean isSlowQuery = false;
-    private boolean updateTotalQueryHistogram = true;
 
     public QueryStatsData(String query, String language) {
         this.query = query;
@@ -144,12 +138,6 @@ public class QueryStatsData {
     public class QueryExecutionStats {
         
         long time;
-        private final long SLOW_QUERY_HISTOGRAM = 1;
-        private final long TOTAL_QUERY_HISTOGRAM = 0;
-        private final String SLOW_QUERY_PERCENTILE_METRICS_NAME = "SLOW_QUERY_PERCENTILE_METRICS";
-        private final String SLOW_QUERY_COUNT_NAME = "SLOW_QUERY_COUNT";
-        private final int SLOW_QUERY_LIMIT_SCANNED =
-                Integer.getInteger("oak.query.slowScanLimit", 100000);
         
         public void execute(long nanos) {
             QueryRecorder.record(query, internal);
@@ -183,22 +171,9 @@ public class QueryStatsData {
             maxTimeNanos = Math.max(maxTimeNanos, time);
         }
 
-        public void scan(long count, long max, QueryEngineSettings queryEngineSettings) {
+        public void scan(long count, long max) {
             totalRowsScanned += count;
             maxRowsScanned = Math.max(maxRowsScanned, max);
-            long maxScannedLimit = Math.min(SLOW_QUERY_LIMIT_SCANNED, queryEngineSettings.getLimitReads());
-            if (updateTotalQueryHistogram) {
-                updateTotalQueryHistogram = false;
-                HistogramStats histogramStats = queryEngineSettings.getStatisticsProvider().getHistogram(SLOW_QUERY_PERCENTILE_METRICS_NAME, StatsOptions.METRICS_ONLY);
-                histogramStats.update(TOTAL_QUERY_HISTOGRAM);
-            }
-            if (totalRowsScanned >= maxScannedLimit && !isSlowQuery) {
-                isSlowQuery = true;
-                HistogramStats histogramStats = queryEngineSettings.getStatisticsProvider().getHistogram(SLOW_QUERY_PERCENTILE_METRICS_NAME, StatsOptions.METRICS_ONLY);
-                histogramStats.update(SLOW_QUERY_HISTOGRAM);
-                CounterStats slowQueryCounter = queryEngineSettings.getStatisticsProvider().getCounterStats(SLOW_QUERY_COUNT_NAME, StatsOptions.METRICS_ONLY);
-                slowQueryCounter.inc();
-            }
         }
     }
 
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/query/SlowQueryMetricTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/query/SlowQueryMetricTest.java
new file mode 100644
index 0000000000..5b764e665f
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/query/SlowQueryMetricTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.jackrabbit.oak.spi.query;
+
+import org.apache.jackrabbit.oak.InitialContent;
+import org.apache.jackrabbit.oak.Oak;
+import org.apache.jackrabbit.oak.api.ContentRepository;
+import org.apache.jackrabbit.oak.api.ContentSession;
+import org.apache.jackrabbit.oak.api.QueryEngine;
+import org.apache.jackrabbit.oak.api.Result;
+import org.apache.jackrabbit.oak.api.ResultRow;
+import org.apache.jackrabbit.oak.api.Root;
+import org.apache.jackrabbit.oak.api.Tree;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.metric.MetricStatisticsProvider;
+import org.apache.jackrabbit.oak.query.QueryEngineSettings;
+import org.apache.jackrabbit.oak.query.RuntimeNodeTraversalException;
+import org.apache.jackrabbit.oak.spi.security.OpenSecurityProvider;
+import org.apache.jackrabbit.oak.spi.whiteboard.DefaultWhiteboard;
+import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
+import org.apache.jackrabbit.oak.stats.CounterStats;
+import org.apache.jackrabbit.oak.stats.HistogramStats;
+import org.apache.jackrabbit.oak.stats.StatisticsProvider;
+import org.apache.jackrabbit.oak.stats.StatsOptions;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.jcr.query.Query;
+import java.lang.management.ManagementFactory;
+import java.text.ParseException;
+import java.util.Collections;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+
+/**
+ * {@code SlowQueryMetricTest} contains slowQuery metrics related tests.
+ */
+public class SlowQueryMetricTest {
+
+    private ContentRepository repository;
+    private ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
+    private MetricStatisticsProvider statsProvider =
+            new MetricStatisticsProvider(ManagementFactory.getPlatformMBeanServer(), executor);
+    private QueryEngineSettings queryEngineSettings = new QueryEngineSettings(statsProvider);
+
+
+    Oak oak = null;
+
+    @Before
+    public void setUp() {
+        queryEngineSettings.setLimitReads(11);
+        Whiteboard whiteboard = new DefaultWhiteboard();
+        whiteboard.register(StatisticsProvider.class, statsProvider, Collections.emptyMap());
+        oak = new Oak().with(new OpenSecurityProvider()).with(new InitialContent()).with(queryEngineSettings).with(whiteboard);
+        repository = oak.createContentRepository();
+    }
+
+    @After
+    public void tearDown() {
+        repository = null;
+        statsProvider.close();
+        new ExecutorCloser(executor).close();
+    }
+
+    private String SLOW_QUERY_COUNT_NAME = "SLOW_QUERY_COUNT";
+    private String SLOW_QUERY_PERCENTILE_METRICS_NAME = "SLOW_QUERY_PERCENTILE_METRICS";
+
+    @Test
+    public void queryOnStableRevision() throws Exception {
+        long maxReadEntries = 1000;//we check for max traversals for each 1000 node reads, see Cursors.java -> fetchNext()
+        ContentSession s = repository.login(null, null);
+        Root r = s.getLatestRoot();
+        Tree t = r.getTree("/").addChild("test");
+        for (int i = 0; i < maxReadEntries + 1; i++) {
+            t.addChild("node" + i).setProperty("jcr:primaryType", "nt:base");
+        }
+        r.commit();
+        ContentSession s2 = repository.login(null, null);
+        Root r2 = s2.getLatestRoot();
+        CounterStats slowQueryCounter = queryEngineSettings.getStatisticsProvider().getCounterStats(SLOW_QUERY_COUNT_NAME, StatsOptions.METRICS_ONLY);
+        HistogramStats histogramStats = queryEngineSettings.getStatisticsProvider().getHistogram(SLOW_QUERY_PERCENTILE_METRICS_NAME, StatsOptions.METRICS_ONLY);
+        long totalQueryCount = histogramStats.getCount();
+        long slowQueryCount = slowQueryCounter.getCount();
+        Assert.assertEquals(totalQueryCount, 0);
+        Assert.assertEquals(slowQueryCount, 0);
+
+        Result result = executeQuery(r2, "test/node1//element(*, nt:base)");
+        for (ResultRow rr : result.getRows()) {
+        }
+        totalQueryCount = histogramStats.getCount();
+        slowQueryCount = slowQueryCounter.getCount();
+        Assert.assertEquals(totalQueryCount, 1);
+        Assert.assertEquals(slowQueryCount, 0);
+
+        executeAndAssertSlowQuery(r2, queryEngineSettings);
+    }
+
+    private void executeAndAssertSlowQuery(Root r2, QueryEngineSettings queryEngineSettings) throws ParseException {
+        Result result = executeQuery(r2, "test//element(*, nt:base)");
+        CounterStats slowQueryCounter = queryEngineSettings.getStatisticsProvider().getCounterStats(SLOW_QUERY_COUNT_NAME, StatsOptions.METRICS_ONLY);
+        HistogramStats histogramStats = queryEngineSettings.getStatisticsProvider().getHistogram(SLOW_QUERY_PERCENTILE_METRICS_NAME, StatsOptions.METRICS_ONLY);
+        long initialSlowQueryCounter = slowQueryCounter.getCount();
+        long initialHistogramCounter = histogramStats.getCount();
+        try {
+            for (ResultRow rr : result.getRows()) {
+            }
+        } catch (RuntimeNodeTraversalException e) {
+
+            /*
+             count increased by 2. one for being a query and one for being slow query. Added twice to get histogram percentile info
+             */
+            Assert.assertEquals(histogramStats.getCount(), initialHistogramCounter + 2);
+            Assert.assertEquals(slowQueryCounter.getCount(), initialSlowQueryCounter + 1);
+            return;
+        }
+        Assert.fail("Unable to catch max Node Traversal limit breach");
+    }
+
+    private Result executeQuery(Root r2, String queryString) throws ParseException {
+        Result result = r2.getQueryEngine().executeQuery(queryString, Query.XPATH,
+                QueryEngine.NO_BINDINGS, QueryEngine.NO_MAPPINGS);
+        return result;
+    }
+}
diff --git a/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/ReopenedLuceneIndexTest.java b/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/ReopenedLuceneIndexTest.java
index 78bc876697..066a99f155 100644
--- a/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/ReopenedLuceneIndexTest.java
+++ b/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/ReopenedLuceneIndexTest.java
@@ -33,6 +33,7 @@ import org.apache.jackrabbit.oak.plugins.index.lucene.util.IndexDefinitionBuilde
 import org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition;
 import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore;
 import org.apache.jackrabbit.oak.query.QueryEngineSettings;
+import org.apache.jackrabbit.oak.query.RuntimeNodeTraversalException;
 import org.apache.jackrabbit.oak.spi.commit.Observer;
 import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider;
 import org.apache.jackrabbit.oak.spi.security.OpenSecurityProvider;
@@ -135,7 +136,7 @@ public class ReopenedLuceneIndexTest {
 
     @Test
     public void resultSizeAboveLimitCompatV1() throws Exception {
-        thrown.expect(UnsupportedOperationException.class);
+        thrown.expect(RuntimeNodeTraversalException.class);
         thrown.expectMessage(StringContains.containsString("The query read or traversed more than " + READ_LIMIT + " nodes. To avoid affecting other tasks, processing was stopped."));
 
         // Add more data such that the query genuinely supasses query limit
@@ -146,7 +147,7 @@ public class ReopenedLuceneIndexTest {
 
     @Test
     public void resultSizeAboveLimitCompatV2() throws Exception {
-        thrown.expect(UnsupportedOperationException.class);
+        thrown.expect(RuntimeNodeTraversalException.class);
         thrown.expectMessage(StringContains.containsString("The query read or traversed more than " + READ_LIMIT + " nodes. To avoid affecting other tasks, processing was stopped."));
 
         // Add more data such that the query genuinely supasses query limit
-- 
2.17.1

