diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt index f54a40e36dd..7211d153b8d 100644 --- a/solr/CHANGES.txt +++ b/solr/CHANGES.txt @@ -248,6 +248,9 @@ Bug Fixes * SOLR-14503: Use specified waitForZk value as connection timeout for zookeeper in SolrDispatcherFilter. Also, consume specified SOLR_WAIT_FOR_ZK in bin/solr.cmd (Colvin Cowie via Munendra S N) +* SOLR-14850: Fix ExactStatsCache NullPointerException when shards.tolerant=true. + (Eugene Tenkaev via ab) + Other Changes --------------------- diff --git a/solr/core/src/java/org/apache/solr/search/stats/ExactStatsCache.java b/solr/core/src/java/org/apache/solr/search/stats/ExactStatsCache.java index a503f054bc2..d11cc618a67 100644 --- a/solr/core/src/java/org/apache/solr/search/stats/ExactStatsCache.java +++ b/solr/core/src/java/org/apache/solr/search/stats/ExactStatsCache.java @@ -94,6 +94,12 @@ public class ExactStatsCache extends StatsCache { protected void doMergeToGlobalStats(SolrQueryRequest req, List responses) { Set allTerms = new HashSet<>(); for (ShardResponse r : responses) { + if ("true".equalsIgnoreCase(req.getParams().get(ShardParams.SHARDS_TOLERANT)) && r.getException() != null) { + // Can't expect stats if there was an exception for this request on any shard + // this should only happen when using shards.tolerant=true + log.debug("Exception shard response={}", r); + continue; + } if (log.isDebugEnabled()) { log.debug("Merging to global stats, shard={}, response={}", r.getShard(), r.getSolrResponse().getResponse()); } diff --git a/solr/core/src/test/org/apache/solr/search/stats/TestExactStatsCache.java b/solr/core/src/test/org/apache/solr/search/stats/TestExactStatsCache.java index bec4747e486..f37232d17a7 100644 --- a/solr/core/src/test/org/apache/solr/search/stats/TestExactStatsCache.java +++ b/solr/core/src/test/org/apache/solr/search/stats/TestExactStatsCache.java @@ -16,9 +16,81 @@ */ package org.apache.solr.search.stats; +import org.apache.solr.client.solrj.SolrClient; +import org.apache.solr.client.solrj.response.QueryResponse; +import org.apache.solr.common.SolrDocumentList; +import org.apache.solr.common.params.ModifiableSolrParams; +import org.apache.solr.common.params.ShardParams; +import org.junit.Test; + public class TestExactStatsCache extends TestBaseStatsCache { + private int docId = 0; + @Override protected String getStatsCacheClassName() { return ExactStatsCache.class.getName(); } -} + + @Test + @ShardsFixed(num = 3) + public void testShardsTolerant() throws Exception { + del("*:*"); + commit(); + for (int i = 0; i < clients.size(); i++) { + int shard = i + 1; + index_specific(i, id, docId++, "a_t", "one two three", + "shard_i", shard); + index_specific(i, id, docId++, "a_t", "one two three four five", + "shard_i", shard); + } + commit(); + int expectedResults = 2 * (clients.size() - 1); + + checkShardsTolerantQuery(expectedResults, "q", "a_t:one", "fl", "*,score"); + } + + protected void checkShardsTolerantQuery(int expectedResults, Object... q) throws Exception { + final ModifiableSolrParams params = new ModifiableSolrParams(); + for (int i = 0; i < q.length; i += 2) { + params.add(q[i].toString(), q[i + 1].toString()); + } + + // query a random server + params.set(ShardParams.SHARDS, getShardsStringWithOneDeadShard()); + params.set(ShardParams.SHARDS_TOLERANT, "true"); + int which = r.nextInt(clients.size()); + SolrClient client = clients.get(which); + QueryResponse rsp = client.query(params); + checkPartialResponse(rsp, expectedResults); + } + + protected String getShardsStringWithOneDeadShard() { + assertNotNull("this test requires deadServers to be non-null", deadServers); + assertTrue("this test requires at least 2 shards", shardsArr.length > 1); + + StringBuilder sb = new StringBuilder(); + // copy over the real shard names except for the last one, + // replace it with a dead server + for (int shardN = 0; shardN < shardsArr.length; shardN++) { + if (sb.length() > 0) sb.append(','); + + String shard; + if (shardN != shardsArr.length - 1) { + shard = shardsArr[shardN]; + } else { + if (deadServers[0].endsWith("/")) shard = deadServers[0] + DEFAULT_TEST_COLLECTION_NAME; + else shard = deadServers[0] + "/" + DEFAULT_TEST_CORENAME; + } + sb.append(shard); + } + + return sb.toString(); + } + + protected void checkPartialResponse(QueryResponse response, int expectedResults) { + assertTrue("should have 'partialResults' in header", (Boolean)response.getHeader().get("partialResults")); + SolrDocumentList docList = response.getResults(); + assertEquals(expectedResults, docList.size()); + assertEquals(expectedResults, docList.getNumFound()); + } +} \ No newline at end of file