madrob commented on a change in pull request #592:
URL: https://github.com/apache/solr/pull/592#discussion_r806175843



##########
File path: solr/core/src/test/org/apache/solr/search/TestMainQueryCaching.java
##########
@@ -0,0 +1,313 @@
+/*
+ * 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.solr.search;
+
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.util.ExecutorUtil;
+import org.apache.solr.common.util.SolrNamedThreadFactory;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.metrics.MetricsMap;
+import org.apache.solr.metrics.SolrMetricManager;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import static org.apache.solr.common.util.Utils.fromJSONString;
+
+/**
+ * Verify caching interactions between main query and filterCache
+ */
+public class TestMainQueryCaching extends SolrTestCaseJ4 {
+
+  private static final int MOST_DOCS = 100;
+  private static final int ALL_DOCS = MOST_DOCS + 1;
+  private static final String TEST_UFFSQ_PROPNAME = 
"solr.test.useFilterForSortedQuery";
+  static String RESTORE_UFFSQ_PROP;
+  static boolean USE_FILTER_FOR_SORTED_QUERY;
+
+  @BeforeClass
+  public static void beforeClass() throws Exception {
+    // TODO: figure out why the line below (accepting this property as 
overridden on test invocation) isn't working
+    //  as expected.
+    final String uffsq = System.getProperty(TEST_UFFSQ_PROPNAME, 
Boolean.toString(random().nextBoolean()));
+    USE_FILTER_FOR_SORTED_QUERY = Boolean.parseBoolean(uffsq);
+    RESTORE_UFFSQ_PROP = System.setProperty(TEST_UFFSQ_PROPNAME, uffsq);
+    initCore("solrconfig-deeppaging.xml", "schema-sorts.xml");
+    createIndex();
+  }
+
+  @AfterClass
+  public static void afterClass() throws Exception {
+    if (RESTORE_UFFSQ_PROP == null) {
+      System.clearProperty(TEST_UFFSQ_PROPNAME);
+    } else {
+      System.setProperty(TEST_UFFSQ_PROPNAME, RESTORE_UFFSQ_PROP);
+    }
+  }
+
+  public static void createIndex() {
+    for (int i = 0; i < MOST_DOCS; i++) {
+      assertU(adoc("id", Integer.toString(i), "str", "d" + i));
+      if (random().nextInt(MOST_DOCS) == 0) {
+        assertU(commit());  // sometimes make multiple segments
+      }
+    }
+    // add an extra doc to distinguish scoring query from `*:*`
+    assertU(adoc("id", Integer.toString(MOST_DOCS), "str", "e" + MOST_DOCS));
+    assertU(commit());
+  }
+
+  @Before
+  public void beforeTest() throws Exception {
+    // testing caching, it's far simpler to just reload the core every time to 
prevent
+    // subsequent requests from affecting each other
+    h.reload();
+  }
+
+  private static long coreToInserts(SolrCore core) {
+    return (long)((MetricsMap)((SolrMetricManager.GaugeWrapper<?>)core
+            
.getCoreMetricManager().getRegistry().getMetrics().get("CACHE.searcher.filterCache")).getGauge())
+            .getValue().get("inserts");
+  }
+
+  private static long coreToSortCount(SolrCore core, String skipOrFull) {
+    return (long)((SolrMetricManager.GaugeWrapper<?>)core
+            
.getCoreMetricManager().getRegistry().getMetrics().get("SEARCHER.searcher." + 
skipOrFull + "SortCount")).getGauge()
+            .getValue();
+  }
+
+  private static long coreToLiveDocsNaiveCacheHitCount(SolrCore core) {
+    return (long)((SolrMetricManager.GaugeWrapper<?>)core
+            
.getCoreMetricManager().getRegistry().getMetrics().get("SEARCHER.searcher.liveDocsNaiveCacheHitCount")).getGauge()
+            .getValue();
+  }
+
+  private static long coreToMatchAllDocsInsertCount(SolrCore core) {
+    return (long) coreToLiveDocsCacheMetrics(core).get("inserts");
+  }
+
+  private static Map<String, Object> coreToLiveDocsCacheMetrics(SolrCore core) 
{
+    return 
((MetricsMap)((SolrMetricManager.GaugeWrapper<?>)core.getCoreMetricManager().getRegistry()
+            
.getMetrics().get("CACHE.searcher.liveDocsCache")).getGauge()).getValue();
+  }
+  private static final String SCORING_QUERY = "str:d*";
+  private static final String CONSTANT_SCORE_QUERY = "(" + SCORING_QUERY + 
")^=1.0"; // wrapped as a ConstantScoreQuery
+  private static final String MATCH_ALL_DOCS_QUERY = "*:*";
+
+  private static final String[] ALL_QUERIES = new String[] { SCORING_QUERY, 
CONSTANT_SCORE_QUERY, MATCH_ALL_DOCS_QUERY };
+
+  @Test
+  public void testScoringQuery() throws Exception {
+    // plain request should have no caching or sorting optimization
+    String response = JQ(req("q", SCORING_QUERY, "indent", "true"));
+    assertMetricCounts(response, false, 0, 1, 0);
+  }
+
+  @Test
+  public void testConstantScoreFlScore() throws Exception {
+    // explicitly requesting scores should unconditionally disable caching and 
sorting optimizations
+    String response = JQ(req("q", CONSTANT_SCORE_QUERY, "indent", "true", 
"rows", "0", "fl", "id,score", "sort", (random().nextBoolean() ? "id asc" : 
"score desc")));
+    assertMetricCounts(response, false, 0, 1, 0);
+  }
+
+  @Test
+  public void testScoringQueryNonScoreSort() throws Exception {
+    // plain request with no score in sort should consult filterCache, but 
need full sorting
+    String response = JQ(req("q", SCORING_QUERY, "indent", "true", "sort", "id 
asc"));
+    assertMetricCounts(response, false, USE_FILTER_FOR_SORTED_QUERY ? 1 : 0, 
1, 0);
+  }
+
+  @Test
+  public void testScoringQueryZeroRows() throws Exception {
+    // always hit cache, optimize sort because rows=0
+    String response = JQ(req("q", SCORING_QUERY, "indent", "true", "rows", 
"0", "sort", (random().nextBoolean() ? "id asc" : "score desc")));
+    final int insertAndSkipCount = USE_FILTER_FOR_SORTED_QUERY ? 1 : 0;
+    assertMetricCounts(response, false, insertAndSkipCount, 
USE_FILTER_FOR_SORTED_QUERY ? 0 : 1, insertAndSkipCount);
+  }
+
+  @Test
+  public void testConstantScoreSortByScore() throws Exception {
+    // hit cache and skip sort because constant score query
+    String response = JQ(req("q", CONSTANT_SCORE_QUERY, "indent", "true"));
+    final int insertAndSkipCount = USE_FILTER_FOR_SORTED_QUERY ? 1 : 0;
+    assertMetricCounts(response, false, insertAndSkipCount, 
USE_FILTER_FOR_SORTED_QUERY ? 0 : 1, insertAndSkipCount);
+  }
+
+  @Test
+  public void testConstantScoreNonScoreSort() throws Exception {
+    // consult filterCache because constant score query, but no skip sort 
(because sort-by-id)
+    String response = JQ(req("q", CONSTANT_SCORE_QUERY, "indent", "true", 
"sort", "id asc"));
+    assertMetricCounts(response, false, USE_FILTER_FOR_SORTED_QUERY ? 1 : 0, 
1, 0);
+  }
+
+  /**
+   * As {@link #testConstantScoreNonScoreSort} (though an analogous test could 
be written corresponding to
+   * {@link #testConstantScoreSortByScore()}, etc...); but with an additional 
constant-score clause that causes
+   * the associated DocSet, (if {@link #USE_FILTER_FOR_SORTED_QUERY}==true) to 
be cached as equivalent to
+   * MatchAllDocsQuery/liveDocs, _in addition to_ in the filterCache.
+   *
+   * This is an edge case, but it's the behavior we want, and despite there 
being two entries, the actual DocSet
+   * will be the same (`==`) in both locations (liveDocs and filterCache)
+   */
+  @Test
+  public void testConstantScoreMatchesAllDocsNonScoreSort() throws Exception {
+    String response = JQ(req("q", CONSTANT_SCORE_QUERY + " OR (str:e*)^=4.0", 
"indent", "true", "sort", "id asc"));
+    assertMetricCounts(response, USE_FILTER_FOR_SORTED_QUERY, 
USE_FILTER_FOR_SORTED_QUERY ? 1 : 0, 1, 0, ALL_DOCS);
+  }
+
+  @Test
+  public void testMatchAllDocsPlain() throws Exception {
+    // plain request with "score" sort should skip sort even if `rows` 
requested
+    String response = JQ(req("q", MATCH_ALL_DOCS_QUERY, "indent", "true"));
+    assertMetricCounts(response, true, 0, 0, 1);
+  }
+
+  @Test
+  public void testMatchAllDocsFlScore() throws Exception {
+    // explicitly requesting scores should unconditionally disable all cache 
consultation and sort optimization
+    String response = JQ(req("q", MATCH_ALL_DOCS_QUERY, "indent", "true", 
"rows", "0", "fl", "id,score", "sort", (random().nextBoolean() ? "id asc" : 
"score desc")));
+    // NOTE: pretend we're not MatchAllDocs ...
+    assertMetricCounts(response, false, 0, 1, 0, ALL_DOCS);
+  }
+
+  @Test
+  public void testMatchAllDocsZeroRows() throws Exception {
+    // plain request should _always_ skip sort when `rows=0`
+    String response = JQ(req("q", MATCH_ALL_DOCS_QUERY, "indent", "true", 
"rows", "0", "sort", "id asc"));
+    assertMetricCounts(response, true, 0, 0, 1);
+  }
+
+  @Test
+  public void testMatchAllDocsNonScoreSort() throws Exception {
+    // plain request _with_ rows and non-score sort should consult cache, but 
not skip sort
+    String response = JQ(req("q", MATCH_ALL_DOCS_QUERY, "indent", "true", 
"sort", "id asc"));
+    assertMetricCounts(response, true, 0, 1, 0);
+  }
+
+  @Test
+  public void testCursorMark() throws Exception {
+    String q = pickRandom(ALL_QUERIES);
+    boolean includeScoreInSort = random().nextBoolean();
+    String response = JQ(req("q", q, "indent", "true", "cursorMark", "*", 
"sort", includeScoreInSort ? "score desc,id asc" : "id asc"));
+    final int expectNumFound = MATCH_ALL_DOCS_QUERY.equals(q) ? ALL_DOCS : 
MOST_DOCS;
+    final boolean consultMatchAllDocs;
+    final boolean insertFilterCache;
+    if (includeScoreInSort) {
+      consultMatchAllDocs = false;
+      insertFilterCache = false;
+    } else if (MATCH_ALL_DOCS_QUERY.equals(q)) {
+      consultMatchAllDocs = true;
+      insertFilterCache = false;
+    } else {
+      consultMatchAllDocs = false;
+      insertFilterCache = USE_FILTER_FOR_SORTED_QUERY;
+    }
+    assertMetricCounts(response, consultMatchAllDocs, insertFilterCache ? 1 : 
0, 1, 0, expectNumFound);
+  }
+
+  @Test
+  public void testCursorMarkZeroRows() throws Exception {
+    String q = pickRandom(ALL_QUERIES);
+    String response = JQ(req("q", q, "indent", "true", "cursorMark", "*", 
"rows", "0", "sort", random().nextBoolean() ? "id asc" : "score desc,id asc"));
+    final boolean consultMatchAllDocs;
+    final boolean insertFilterCache;
+    final boolean skipSort;
+    if (MATCH_ALL_DOCS_QUERY.equals(q)) {
+      consultMatchAllDocs = true;
+      insertFilterCache = false;
+      skipSort = true;
+    } else {
+      consultMatchAllDocs = false;
+      insertFilterCache = USE_FILTER_FOR_SORTED_QUERY;
+      skipSort = USE_FILTER_FOR_SORTED_QUERY;
+    }
+    assertMetricCounts(response, consultMatchAllDocs, insertFilterCache ? 1 : 
0, skipSort ? 0 : 1, skipSort ? 1 : 0);
+  }
+
+  private static void assertMetricCounts(String response, boolean 
matchAllDocs, int expectFilterCacheInsertCount, int expectFullSortCount, int 
expectSkipSortCount) {
+    assertMetricCounts(response, matchAllDocs, expectFilterCacheInsertCount, 
expectFullSortCount, expectSkipSortCount, matchAllDocs ? ALL_DOCS : MOST_DOCS);
+  }
+
+  private static void assertMetricCounts(String response, boolean 
matchAllDocs, int expectFilterCacheInsertCount,
+                                         int expectFullSortCount, int 
expectSkipSortCount, int expectNumFound) {
+    Map<?, ?> res = (Map<?, ?>) fromJSONString(response);
+    Map<?, ?> body = (Map<?, ?>) (res.get("response"));
+    SolrCore core = h.getCore();
+    assertEquals("Bad matchAllDocs insert count", (matchAllDocs ? 1 : 0), 
coreToMatchAllDocsInsertCount(core));
+    assertEquals("Bad filterCache insert count", expectFilterCacheInsertCount, 
coreToInserts(core));
+    assertEquals("Bad full sort count", expectFullSortCount, 
coreToSortCount(core, "full"));
+    assertEquals("Bad skip sort count", expectSkipSortCount, 
coreToSortCount(core, "skip"));
+    assertEquals("Should have exactly " + expectNumFound, expectNumFound, 
(long) (body.get("numFound"))); // sanity check
+  }
+
+  @Test
+  public void testConcurrentMatchAllDocsInitialization() throws Exception {
+    final int nThreads = 20;
+    final ExecutorService executor = 
ExecutorUtil.newMDCAwareFixedThreadPool(nThreads, new 
SolrNamedThreadFactory(getTestName()));
+    final Future<?>[] followup = new Future<?>[nThreads];
+    for (int i = 0; i < nThreads; i++) {
+      final int myI = i;
+      followup[i] = executor.submit(() -> {
+        try {
+          // NOTE: we use cursorMark=* here because it prevents consulting the 
queryResultCache, which can interfere
+          // with DocSet fetching (which is what we care about in this test).
+          String response = JQ(req("q", MATCH_ALL_DOCS_QUERY, "request_id", 
Integer.toString(myI), "cursorMark", "*", "sort", "id asc"));
+          Map<?, ?> res = (Map<?, ?>) fromJSONString(response);
+          Map<?, ?> body = (Map<?, ?>) (res.get("response"));
+          assertEquals("Should have exactly " + ALL_DOCS, ALL_DOCS, (long) 
(body.get("numFound"))); // sanity check
+        } catch (Exception ex) {
+          throw new RuntimeException(ex);
+        }
+      });
+    }
+    try {
+      for (Future<?> f : followup) {
+        f.get(); // to access exceptions/errors
+      }
+    } finally {
+      executor.shutdown();
+      assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); // tasks 
should already have completed
+    }
+    final SolrCore core = h.getCore();
+    Map<String, Object> liveDocsCacheMetrics = 
coreToLiveDocsCacheMetrics(core);
+    long inserts = (long) liveDocsCacheMetrics.get("inserts"); // the one and 
only liveDocs computation
+    long hits = (long) liveDocsCacheMetrics.get("hits"); // hits during the 
initial phase
+    long asyncHits = (long) liveDocsCacheMetrics.get("asyncHits");

Review comment:
       Yea, I can go either way. My thought was that if the constant ever 
moves, or the metric path moves, we'd want tests to fail compilation until they 
are updated. But this makes sense too. I'm not committed to either way.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: issues-unsubscr...@solr.apache.org

For queries about this service, please contact Infrastructure at:
us...@infra.apache.org



---------------------------------------------------------------------
To unsubscribe, e-mail: issues-unsubscr...@solr.apache.org
For additional commands, e-mail: issues-h...@solr.apache.org

Reply via email to