| /* |
| * 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.handler.component; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| |
| import org.apache.commons.io.FileUtils; |
| import org.apache.solr.SolrJettyTestBase; |
| import org.apache.solr.client.solrj.SolrClient; |
| import org.apache.solr.client.solrj.SolrQuery; |
| import org.apache.solr.client.solrj.SolrServerException; |
| import org.apache.solr.client.solrj.impl.HttpSolrClient; |
| import org.apache.solr.client.solrj.request.CoreAdminRequest; |
| import org.apache.solr.client.solrj.response.QueryResponse; |
| import org.apache.solr.common.SolrException; |
| import org.apache.solr.common.SolrInputDocument; |
| import org.apache.solr.common.params.ShardParams; |
| import org.apache.solr.common.util.NamedList; |
| import org.apache.solr.response.SolrQueryResponse; |
| import org.junit.AfterClass; |
| import org.junit.BeforeClass; |
| import org.junit.Test; |
| |
| public class DistributedDebugComponentTest extends SolrJettyTestBase { |
| |
| private static SolrClient collection1; |
| private static SolrClient collection2; |
| private static String shard1; |
| private static String shard2; |
| private static File solrHome; |
| |
| private static File createSolrHome() throws Exception { |
| File workDir = createTempDir().toFile(); |
| setupJettyTestHome(workDir, "collection1"); |
| FileUtils.copyDirectory(new File(workDir, "collection1"), new File(workDir, "collection2")); |
| return workDir; |
| } |
| |
| |
| @BeforeClass |
| public static void createThings() throws Exception { |
| systemSetPropertySolrDisableShardsWhitelist("true"); |
| solrHome = createSolrHome(); |
| createAndStartJetty(solrHome.getAbsolutePath()); |
| String url = jetty.getBaseUrl().toString(); |
| |
| collection1 = getHttpSolrClient(url + "/collection1"); |
| collection2 = getHttpSolrClient(url + "/collection2"); |
| |
| String urlCollection1 = jetty.getBaseUrl().toString() + "/" + "collection1"; |
| String urlCollection2 = jetty.getBaseUrl().toString() + "/" + "collection2"; |
| shard1 = urlCollection1.replaceAll("https?://", ""); |
| shard2 = urlCollection2.replaceAll("https?://", ""); |
| |
| //create second core |
| try (HttpSolrClient nodeClient = getHttpSolrClient(url)) { |
| CoreAdminRequest.Create req = new CoreAdminRequest.Create(); |
| req.setCoreName("collection2"); |
| req.setConfigSet("collection1"); |
| nodeClient.request(req); |
| } |
| |
| SolrInputDocument doc = new SolrInputDocument(); |
| doc.setField("id", "1"); |
| doc.setField("text", "batman"); |
| collection1.add(doc); |
| collection1.commit(); |
| |
| doc.setField("id", "2"); |
| doc.setField("text", "superman"); |
| collection2.add(doc); |
| collection2.commit(); |
| |
| } |
| |
| @AfterClass |
| public static void destroyThings() throws Exception { |
| if (null != collection1) { |
| collection1.close(); |
| collection1 = null; |
| } |
| if (null != collection2) { |
| collection2.close(); |
| collection2 = null; |
| } |
| if (null != jetty) { |
| jetty.stop(); |
| jetty=null; |
| } |
| resetExceptionIgnores(); |
| systemClearPropertySolrDisableShardsWhitelist(); |
| } |
| |
| @Test |
| @SuppressWarnings("unchecked") |
| public void testSimpleSearch() throws Exception { |
| SolrQuery query = new SolrQuery(); |
| query.setQuery("*:*"); |
| query.set("debug", "track"); |
| query.set("distrib", "true"); |
| query.setFields("id", "text"); |
| query.set("shards", shard1 + "," + shard2); |
| |
| if (random().nextBoolean()) { |
| query.add("omitHeader", Boolean.toString(random().nextBoolean())); |
| } |
| QueryResponse response = collection1.query(query); |
| NamedList<Object> track = (NamedList<Object>) response.getDebugMap().get("track"); |
| assertNotNull(track); |
| assertNotNull(track.get("rid")); |
| assertNotNull(track.get("EXECUTE_QUERY")); |
| assertNotNull(((NamedList<Object>)track.get("EXECUTE_QUERY")).get(shard1)); |
| assertNotNull(((NamedList<Object>)track.get("EXECUTE_QUERY")).get(shard2)); |
| |
| assertNotNull(((NamedList<Object>)track.get("GET_FIELDS")).get(shard1)); |
| assertNotNull(((NamedList<Object>)track.get("GET_FIELDS")).get(shard2)); |
| |
| assertElementsPresent((NamedList<String>)((NamedList<Object>)track.get("EXECUTE_QUERY")).get(shard1), |
| "QTime", "ElapsedTime", "RequestPurpose", "NumFound", "Response"); |
| assertElementsPresent((NamedList<String>)((NamedList<Object>)track.get("EXECUTE_QUERY")).get(shard2), |
| "QTime", "ElapsedTime", "RequestPurpose", "NumFound", "Response"); |
| |
| assertElementsPresent((NamedList<String>)((NamedList<Object>)track.get("GET_FIELDS")).get(shard1), |
| "QTime", "ElapsedTime", "RequestPurpose", "NumFound", "Response"); |
| assertElementsPresent((NamedList<String>)((NamedList<Object>)track.get("GET_FIELDS")).get(shard2), |
| "QTime", "ElapsedTime", "RequestPurpose", "NumFound", "Response"); |
| |
| query.setQuery("id:1"); |
| response = collection1.query(query); |
| track = (NamedList<Object>) response.getDebugMap().get("track"); |
| assertNotNull(((NamedList<Object>)track.get("EXECUTE_QUERY")).get(shard1)); |
| assertNotNull(((NamedList<Object>)track.get("EXECUTE_QUERY")).get(shard2)); |
| |
| assertNotNull(((NamedList<Object>)track.get("GET_FIELDS")).get(shard1)); |
| // This test is invalid, as GET_FIELDS should not be executed in shard 2 |
| assertNull(((NamedList<Object>)track.get("GET_FIELDS")).get(shard2)); |
| } |
| |
| @Test |
| @SuppressWarnings("resource") // Cannot close client in this loop! |
| public void testRandom() throws Exception { |
| final int NUM_ITERS = atLeast(50); |
| |
| for (int i = 0; i < NUM_ITERS; i++) { |
| final SolrClient client = random().nextBoolean() ? collection1 : collection2; |
| |
| SolrQuery q = new SolrQuery(); |
| q.set("distrib", "true"); |
| q.setFields("id", "text"); |
| |
| boolean shard1Results = random().nextBoolean(); |
| boolean shard2Results = random().nextBoolean(); |
| |
| String qs = "_query_with_no_results_"; |
| if (shard1Results) { |
| qs += " OR batman"; |
| } |
| if (shard2Results) { |
| qs += " OR superman"; |
| } |
| q.setQuery(qs); |
| |
| Set<String> shards = new HashSet<String>(Arrays.asList(shard1, shard2)); |
| if (random().nextBoolean()) { |
| shards.remove(shard1); |
| } else if (random().nextBoolean()) { |
| shards.remove(shard2); |
| } |
| q.set("shards", String.join(",", shards)); |
| |
| |
| List<String> debug = new ArrayList<String>(10); |
| |
| boolean all = false; |
| final boolean timing = random().nextBoolean(); |
| final boolean query = random().nextBoolean(); |
| final boolean results = random().nextBoolean(); |
| final boolean track = random().nextBoolean(); |
| |
| if (timing) { debug.add("timing"); } |
| if (query) { debug.add("query"); } |
| if (results) { debug.add("results"); } |
| if (track) { debug.add("track"); } |
| if (debug.isEmpty()) { |
| debug.add("true"); |
| all = true; |
| } |
| q.set("debug", debug.toArray(new String[debug.size()])); |
| |
| QueryResponse r = client.query(q); |
| try { |
| assertDebug(r, all || track, "track"); |
| assertDebug(r, all || query, "rawquerystring"); |
| assertDebug(r, all || query, "querystring"); |
| assertDebug(r, all || query, "parsedquery"); |
| assertDebug(r, all || query, "parsedquery_toString"); |
| assertDebug(r, all || query, "QParser"); |
| assertDebug(r, all || results, "explain"); |
| assertDebug(r, all || timing, "timing"); |
| } catch (AssertionError e) { |
| throw new AssertionError(q.toString() + ": " + e.getMessage(), e); |
| } |
| } |
| } |
| |
| /** |
| * Asserts that the specified debug result key does or does not exist in the |
| * response based on the expected boolean. |
| */ |
| private void assertDebug(QueryResponse response, boolean expected, String key) { |
| if (expected) { |
| assertInDebug(response, key); |
| } else { |
| assertNotInDebug(response, key); |
| } |
| } |
| /** |
| * Asserts that the specified debug result key does exist in the response and is non-null |
| */ |
| private void assertInDebug(QueryResponse response, String key) { |
| assertNotNull("debug map is null", response.getDebugMap()); |
| assertNotNull("debug map has null for : " + key, response.getDebugMap().get(key)); |
| } |
| |
| /** |
| * Asserts that the specified debug result key does NOT exist in the response |
| */ |
| private void assertNotInDebug(QueryResponse response, String key) { |
| assertNotNull("debug map is null", response.getDebugMap()); |
| assertFalse("debug map contains: " + key, response.getDebugMap().containsKey(key)); |
| } |
| |
| |
| @Test |
| public void testDebugSections() throws Exception { |
| SolrQuery query = new SolrQuery(); |
| query.setQuery("text:_query_with_no_results_"); |
| query.set("distrib", "true"); |
| query.setFields("id", "text"); |
| query.set("shards", shard1 + "," + shard2); |
| verifyDebugSections(query, collection1); |
| query.setQuery("id:1 OR text:_query_with_no_results_ OR id:[0 TO 300]"); |
| verifyDebugSections(query, collection1); |
| |
| } |
| |
| private void verifyDebugSections(SolrQuery query, SolrClient client) throws SolrServerException, IOException { |
| query.set("debugQuery", "true"); |
| query.remove("debug"); |
| QueryResponse response = client.query(query); |
| assertFalse(response.getDebugMap().isEmpty()); |
| assertInDebug(response, "track"); |
| assertInDebug(response, "rawquerystring"); |
| assertInDebug(response, "querystring"); |
| assertInDebug(response, "parsedquery"); |
| assertInDebug(response, "parsedquery_toString"); |
| assertInDebug(response, "QParser"); |
| assertInDebug(response, "explain"); |
| assertInDebug(response, "timing"); |
| |
| query.set("debug", "true"); |
| query.remove("debugQuery"); |
| response = client.query(query); |
| assertFalse(response.getDebugMap().isEmpty()); |
| assertInDebug(response, "track"); |
| assertInDebug(response, "rawquerystring"); |
| assertInDebug(response, "querystring"); |
| assertInDebug(response, "parsedquery"); |
| assertInDebug(response, "parsedquery_toString"); |
| assertInDebug(response, "QParser"); |
| assertInDebug(response, "explain"); |
| assertInDebug(response, "timing"); |
| |
| query.set("debug", "track"); |
| response = client.query(query); |
| assertFalse(response.getDebugMap().isEmpty()); |
| assertInDebug(response, "track"); |
| assertNotInDebug(response, "rawquerystring"); |
| assertNotInDebug(response, "querystring"); |
| assertNotInDebug(response, "parsedquery"); |
| assertNotInDebug(response, "parsedquery_toString"); |
| assertNotInDebug(response, "QParser"); |
| assertNotInDebug(response, "explain"); |
| assertNotInDebug(response, "timing"); |
| |
| query.set("debug", "query"); |
| response = client.query(query); |
| assertFalse(response.getDebugMap().isEmpty()); |
| assertNotInDebug(response, "track"); |
| assertInDebug(response, "rawquerystring"); |
| assertInDebug(response, "querystring"); |
| assertInDebug(response, "parsedquery"); |
| assertInDebug(response, "parsedquery_toString"); |
| assertInDebug(response, "QParser"); |
| assertNotInDebug(response, "explain"); |
| assertNotInDebug(response, "timing"); |
| |
| query.set("debug", "results"); |
| response = client.query(query); |
| assertFalse(response.getDebugMap().isEmpty()); |
| assertNotInDebug(response, "track"); |
| assertNotInDebug(response, "rawquerystring"); |
| assertNotInDebug(response, "querystring"); |
| assertNotInDebug(response, "parsedquery"); |
| assertNotInDebug(response, "parsedquery_toString"); |
| assertNotInDebug(response, "QParser"); |
| assertInDebug(response, "explain"); |
| assertNotInDebug(response, "timing"); |
| |
| query.set("debug", "timing"); |
| response = client.query(query); |
| assertFalse(response.getDebugMap().isEmpty()); |
| assertNotInDebug(response, "track"); |
| assertNotInDebug(response, "rawquerystring"); |
| assertNotInDebug(response, "querystring"); |
| assertNotInDebug(response, "parsedquery"); |
| assertNotInDebug(response, "parsedquery_toString"); |
| assertNotInDebug(response, "QParser"); |
| assertNotInDebug(response, "explain"); |
| assertInDebug(response, "timing"); |
| |
| query.set("debug", "false"); |
| response = client.query(query); |
| assertNull(response.getDebugMap()); |
| } |
| |
| @Test |
| // commented out on: 24-Dec-2018 @BadApple(bugUrl="https://issues.apache.org/jira/browse/SOLR-12028") // added 20-Sep-2018 |
| public void testCompareWithNonDistributedRequest() throws SolrServerException, IOException { |
| SolrQuery query = new SolrQuery(); |
| query.setQuery("id:1 OR id:2"); |
| query.setFilterQueries("id:[0 TO 10]", "id:[0 TO 5]"); |
| query.setRows(1); |
| query.setSort("id", SolrQuery.ORDER.asc); // thus only return id:1 since rows 1 |
| query.set("debug", "true"); |
| query.set("distrib", "true"); |
| query.setFields("id"); |
| if (random().nextBoolean()) { // can affect rb.onePassDistributedQuery |
| query.addField("text"); |
| } |
| query.set(ShardParams.DISTRIB_SINGLE_PASS, random().nextBoolean()); |
| query.set("shards", shard1 + "," + shard2); |
| QueryResponse distribResponse = collection1.query(query); |
| |
| // same query but not distributed |
| query.set("distrib", "false"); |
| query.remove("shards"); |
| QueryResponse nonDistribResponse = collection1.query(query); |
| |
| assertNotNull(distribResponse.getDebugMap().get("track")); |
| assertNull(nonDistribResponse.getDebugMap().get("track")); |
| assertEquals(distribResponse.getDebugMap().size() - 1, nonDistribResponse.getDebugMap().size()); |
| |
| assertSectionEquals(distribResponse, nonDistribResponse, "explain"); |
| assertSectionEquals(distribResponse, nonDistribResponse, "rawquerystring"); |
| assertSectionEquals(distribResponse, nonDistribResponse, "querystring"); |
| assertSectionEquals(distribResponse, nonDistribResponse, "parsedquery"); |
| assertSectionEquals(distribResponse, nonDistribResponse, "parsedquery_toString"); |
| assertSectionEquals(distribResponse, nonDistribResponse, "QParser"); |
| assertSectionEquals(distribResponse, nonDistribResponse, "filter_queries"); |
| assertSectionEquals(distribResponse, nonDistribResponse, "parsed_filter_queries"); |
| |
| // timing should have the same sections: |
| assertSameKeys((NamedList<?>)nonDistribResponse.getDebugMap().get("timing"), (NamedList<?>)distribResponse.getDebugMap().get("timing")); |
| } |
| |
| public void testTolerantSearch() throws SolrServerException, IOException { |
| String badShard = DEAD_HOST_1; |
| SolrQuery query = new SolrQuery(); |
| query.setQuery("*:*"); |
| query.set("debug", "true"); |
| query.set("distrib", "true"); |
| query.setFields("id", "text"); |
| query.set("shards", shard1 + "," + shard2 + "," + badShard); |
| |
| // verify that the request would fail if shards.tolerant=false |
| ignoreException("Server refused connection"); |
| expectThrows(SolrException.class, () -> collection1.query(query)); |
| |
| query.set(ShardParams.SHARDS_TOLERANT, "true"); |
| QueryResponse response = collection1.query(query); |
| assertTrue((Boolean)response.getResponseHeader().get(SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY)); |
| @SuppressWarnings("unchecked") |
| NamedList<String> badShardTrack = |
| (((NamedList<NamedList<NamedList<String>>>)response.getDebugMap().get("track")).get("EXECUTE_QUERY")).get(badShard); |
| assertEquals("Unexpected response size for shard", 1, badShardTrack.size()); |
| Entry<String, String> exception = badShardTrack.iterator().next(); |
| assertEquals("Expected key 'Exception' not found", "Exception", exception.getKey()); |
| assertNotNull("Exception message should not be null", exception.getValue()); |
| unIgnoreException("Server refused connection"); |
| } |
| |
| /** |
| * Compares the same section on the two query responses |
| */ |
| private void assertSectionEquals(QueryResponse distrib, QueryResponse nonDistrib, String section) { |
| assertEquals(section + " debug should be equal", distrib.getDebugMap().get(section), nonDistrib.getDebugMap().get(section)); |
| } |
| |
| @SuppressWarnings({"unchecked", "rawtypes"}) |
| private void assertSameKeys(NamedList object, NamedList object2) { |
| Iterator<Map.Entry<String,Object>> iteratorObj2 = (object2).iterator(); |
| for (Map.Entry<String,Object> entry:(NamedList<Object>)object) { |
| assertTrue(iteratorObj2.hasNext()); |
| Map.Entry<String,Object> entry2 = iteratorObj2.next(); |
| assertEquals(entry.getKey(), entry2.getKey()); |
| if (entry.getValue() instanceof NamedList) { |
| assertTrue(entry2.getValue() instanceof NamedList); |
| assertSameKeys((NamedList)entry.getValue(), (NamedList)entry2.getValue()); |
| } |
| } |
| assertFalse(iteratorObj2.hasNext()); |
| } |
| |
| private void assertElementsPresent(NamedList<String> namedList, String...elements) { |
| for(String element:elements) { |
| String value = namedList.get(element); |
| assertNotNull("Expected element '" + element + "' but was not found", value); |
| assertTrue("Expected element '" + element + "' but was empty", !value.isEmpty()); |
| } |
| } |
| |
| } |