This is an automated email from the ASF dual-hosted git repository.

gus pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/branch_9x by this push:
     new 6dbd8d1b8e0 SOLR-17158 Terminate distributed processing faster when 
query limit is reached and partial results are not needed (#2666) (#2773)
6dbd8d1b8e0 is described below

commit 6dbd8d1b8e0e1690b048e32bf152837b5a341fee
Author: Gus Heck <[email protected]>
AuthorDate: Wed Oct 16 10:16:05 2024 -0400

    SOLR-17158 Terminate distributed processing faster when query limit is 
reached and partial results are not needed (#2666) (#2773)
    
    (cherry picked from commit b748207bc331b5eeae284ee7602626dbe5e3ff50)
---
 solr/CHANGES.txt                                   |   6 +
 .../apache/solr/handler/RequestHandlerBase.java    |   4 +-
 .../solr/handler/component/HttpShardHandler.java   | 234 +++++++++++++++++----
 .../component/ParallelHttpShardHandler.java        |  97 +++++----
 .../solr/handler/component/QueryComponent.java     |   7 +-
 .../solr/handler/component/ResponseBuilder.java    |   4 +-
 .../solr/handler/component/SearchHandler.java      |  20 +-
 .../solr/handler/component/ShardHandler.java       |  10 +-
 .../solr/handler/component/ShardResponse.java      |   7 +-
 .../org/apache/solr/request/SolrQueryRequest.java  |  35 +++
 .../org/apache/solr/request/SolrRequestInfo.java   |   9 +
 .../apache/solr/response/SolrQueryResponse.java    |  58 ++++-
 .../java/org/apache/solr/search/QueryLimits.java   |  19 +-
 .../java/org/apache/solr/search/QueryResult.java   |  12 +-
 .../org/apache/solr/search/facet/FacetModule.java  |   8 +-
 .../solr/search/grouping/CommandHandler.java       |  16 +-
 .../SearchGroupShardResponseProcessor.java         |   5 +-
 .../TopGroupsShardResponseProcessor.java           |   5 +-
 .../apache/solr/search/stats/ExactStatsCache.java  |   4 +-
 .../org/apache/solr/util/SolrResponseUtil.java     |   4 +-
 .../apache/solr/search/TestCpuAllowedLimit.java    |  24 ++-
 .../query-guide/pages/common-query-parameters.adoc |  15 +-
 .../pages/major-changes-in-solr-9.adoc             |   6 +
 .../org/apache/solr/common/params/ShardParams.java |  26 +--
 .../apache/solr/common/params/ShardParamsTest.java |  15 +-
 25 files changed, 490 insertions(+), 160 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 3617556a599..9d5dacef1ac 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -15,6 +15,12 @@ New Features
 
 Improvements
 ---------------------
+* SOLR-17158: Users using query limits (timeAllowed, cpuTimeAllowed) for whom 
partial results are uninteresting
+  may set partialResults=false. This parameter has been enhanced to reduce 
time spent processing partial results
+  and omit partialResults from the response. Since this is requested behavior, 
no exception is thrown and the
+  partialResults response header will always exist if the result was short 
circuited.
+  (Gus Heck, Andrzej Bialecki, hossman)
+
 * SOLR-17397: SkipExistingDocumentsProcessor now functions correctly with 
child documents.  (Tim Owens via Eric Pugh)
 
 * SOLR-17180: Deprecate snapshotscli.sh in favour of bin/solr snapshot sub 
commands.  Now able to manage Snapshots from the CLI.  HDFS module specific 
snapshot script now ships as part of that module in the modules/hdfs/bin 
directory. (Eric Pugh)
diff --git a/solr/core/src/java/org/apache/solr/handler/RequestHandlerBase.java 
b/solr/core/src/java/org/apache/solr/handler/RequestHandlerBase.java
index d6a7334449a..b0c17ac687a 100644
--- a/solr/core/src/java/org/apache/solr/handler/RequestHandlerBase.java
+++ b/solr/core/src/java/org/apache/solr/handler/RequestHandlerBase.java
@@ -17,6 +17,7 @@
 package org.apache.solr.handler;
 
 import static org.apache.solr.core.RequestParams.USEPARAM;
+import static org.apache.solr.response.SolrQueryResponse.haveCompleteResults;
 
 import com.codahale.metrics.Counter;
 import com.codahale.metrics.Meter;
@@ -225,7 +226,8 @@ public abstract class RequestHandlerBase
       rsp.setHttpCaching(httpCaching);
       handleRequestBody(req, rsp);
       // count timeouts
-      if (rsp.isPartialResults()) {
+
+      if (!haveCompleteResults(rsp.getResponseHeader())) {
         metrics.numTimeouts.mark();
         rsp.setHttpCaching(false);
       }
diff --git 
a/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandler.java 
b/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandler.java
index 3bc1c542906..219197d8cd8 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandler.java
@@ -16,6 +16,9 @@
  */
 package org.apache.solr.handler.component;
 
+import static org.apache.solr.common.params.CommonParams.PARTIAL_RESULTS;
+import static org.apache.solr.request.SolrQueryRequest.disallowPartialResults;
+
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -24,6 +27,7 @@ import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiConsumer;
 import net.jcip.annotations.NotThreadSafe;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrResponse;
@@ -42,6 +46,7 @@ import org.apache.solr.common.params.ModifiableSolrParams;
 import org.apache.solr.common.params.ShardParams;
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.StrUtils;
 import org.apache.solr.core.CoreDescriptor;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.request.SolrRequestInfo;
@@ -60,6 +65,7 @@ import org.apache.solr.security.AllowListUrlChecker;
  */
 @NotThreadSafe
 public class HttpShardHandler extends ShardHandler {
+
   /**
    * If the request context map has an entry with this key and Boolean.TRUE as 
value, {@link
    * #prepDistributed(ResponseBuilder)} will only include {@link
@@ -70,11 +76,43 @@ public class HttpShardHandler extends ShardHandler {
    */
   public static String ONLY_NRT_REPLICAS = "distribOnlyRealtime";
 
-  protected HttpShardHandlerFactory httpShardHandlerFactory;
+  private final HttpShardHandlerFactory httpShardHandlerFactory;
+
+  /*
+   * Three critical fields:
+   *  - pending: keeps track of how many things we started
+   *  - responseFutureMap: holds futures for anything not yet complete
+   *  - responses: the result of things we started, when responses.size()
+   *
+   * All of this must be kept consistent and is therefore synchronized on 
RESPONSES_LOCK
+   * The exception is when a response is added so long as pending is 
incremented first
+   * because responses is a LinkedBlockingQueue and that is synchronized. The 
response
+   * future map is not synchronized however, and so we need to guard it for 
both order
+   * and memory consistency (happens before) reasons.
+   *
+   * The code works by looping/decrementing pending until responses.size() 
matches the
+   * size of the shard list. Thus, there is a tricky, hidden assumption of one 
response
+   * for every shard, even if the shard is down (so we add a fake response 
with a shard
+   * down exception). Note that down shards have a shard url of empty string 
in this case.
+   *
+   * This seems overcomplicated. Perhaps this can someday be changed to simply
+   * test responses.size == pending.size?
+   */
+  protected final Object FUTURE_MAP_LOCK = new Object();
+
   protected Map<ShardResponse, CompletableFuture<LBSolrClient.Rsp>> 
responseFutureMap;
   protected BlockingQueue<ShardResponse> responses;
+
+  /**
+   * The number of pending requests. This must be incremented before a {@link 
ShardResponse} is
+   * added to {@link #responses}, and decremented after a ShardResponse is 
removed from {@code
+   * responses}. We can't rely on responseFutureMap.size() because it is an 
unsynchronized
+   * collection updated by multiple threads, and it's internal state including 
the size field is not
+   * volatile/synchronized.
+   */
   protected AtomicInteger pending;
-  protected Map<String, List<String>> shardToURLs;
+
+  private final Map<String, List<String>> shardToURLs;
   protected LBHttp2SolrClient lbClient;
 
   public HttpShardHandler(HttpShardHandlerFactory httpShardHandlerFactory) {
@@ -91,6 +129,39 @@ public class HttpShardHandler extends ShardHandler {
     shardToURLs = new HashMap<>();
   }
 
+  /**
+   * Parse the {@value ShardParams#SHARDS_TOLERANT} param from 
<code>params</code> as a boolean;
+   * accepts {@value ShardParams#REQUIRE_ZK_CONNECTED} as a valid value 
indicating <code>false
+   * </code>.
+   *
+   * <p>By default, returns <code>false</code> when {@value 
ShardParams#SHARDS_TOLERANT} is not set
+   * in <code>
+   * params</code>.
+   */
+  public static boolean getShardsTolerantAsBool(SolrQueryRequest req) {
+    String shardsTolerantValue = 
req.getParams().get(ShardParams.SHARDS_TOLERANT);
+    if (null == shardsTolerantValue
+        || 
shardsTolerantValue.trim().equals(ShardParams.REQUIRE_ZK_CONNECTED)) {
+      return false;
+    } else {
+      boolean tolerant = StrUtils.parseBool(shardsTolerantValue.trim());
+      if (tolerant && disallowPartialResults(req.getParams())) {
+        throw new SolrException(
+            SolrException.ErrorCode.BAD_REQUEST,
+            "Use of "
+                + ShardParams.SHARDS_TOLERANT
+                + " requires that "
+                + PARTIAL_RESULTS
+                + " is true. If "
+                + PARTIAL_RESULTS
+                + " is defaulted to false explicitly passing "
+                + PARTIAL_RESULTS
+                + "=true in the request will allow shards.tolerant to work");
+      }
+      return tolerant; // throw an exception if non-boolean
+    }
+  }
+
   public static class SimpleSolrResponse extends SolrResponse {
 
     volatile long elapsedTime;
@@ -120,7 +191,7 @@ public class HttpShardHandler extends ShardHandler {
 
   // Not thread safe... don't use in Callable.
   // Don't modify the returned URL list.
-  protected List<String> getURLs(String shard) {
+  private List<String> getURLs(String shard) {
     List<String> urls = shardToURLs.get(shard);
     if (urls == null) {
       urls = httpShardHandlerFactory.buildURLList(shard);
@@ -129,11 +200,11 @@ public class HttpShardHandler extends ShardHandler {
     return urls;
   }
 
-  protected LBSolrClient.Req prepareLBRequest(
+  private LBSolrClient.Req prepareLBRequest(
       ShardRequest sreq, String shard, ModifiableSolrParams params, 
List<String> urls) {
     params.remove(CommonParams.WT); // use default (currently javabin)
     params.remove(CommonParams.VERSION);
-    QueryRequest req = makeQueryRequest(sreq, params, shard);
+    QueryRequest req = createQueryRequest(sreq, params, shard);
     req.setMethod(SolrRequest.METHOD.POST);
     SolrRequestInfo requestInfo = SolrRequestInfo.getRequestInfo();
     if (requestInfo != null) {
@@ -143,7 +214,7 @@ public class HttpShardHandler extends ShardHandler {
     return httpShardHandlerFactory.newLBHttpSolrClientReq(req, urls);
   }
 
-  protected ShardResponse prepareShardResponse(ShardRequest sreq, String 
shard) {
+  private ShardResponse prepareShardResponse(ShardRequest sreq, String shard) {
     ShardResponse srsp = new ShardResponse();
     if (sreq.nodeName != null) {
       srsp.setNodeName(sreq.nodeName);
@@ -154,14 +225,19 @@ public class HttpShardHandler extends ShardHandler {
     return srsp;
   }
 
-  protected void recordNoUrlShardResponse(ShardResponse srsp, String shard) {
+  private void recordNoUrlShardResponse(ShardResponse srsp, String shard) {
     // TODO: what's the right error code here? We should use the same thing 
when
     // all of the servers for a shard are down.
+    // TODO: shard is a blank string in this case, which is somewhat less than 
helpful
     SolrException exception =
         new SolrException(
             SolrException.ErrorCode.SERVICE_UNAVAILABLE, "no servers hosting 
shard: " + shard);
     srsp.setException(exception);
     srsp.setResponseCode(exception.code());
+
+    // order of next two statements is important. Both are synchronized 
objects so
+    // synchronization is needed so long as the order is correct.
+    pending.incrementAndGet();
     responses.add(srsp);
   }
 
@@ -173,46 +249,60 @@ public class HttpShardHandler extends ShardHandler {
     final var srsp = prepareShardResponse(sreq, shard);
     final var ssr = new SimpleSolrResponse();
     srsp.setSolrResponse(ssr);
-    pending.incrementAndGet();
-
     if (urls.isEmpty()) {
       recordNoUrlShardResponse(srsp, shard);
       return;
     }
+    long startTimeNS = System.nanoTime();
 
-    long startTime = System.nanoTime();
-    CompletableFuture<LBSolrClient.Rsp> future = 
this.lbClient.requestAsync(lbReq);
-    future.whenComplete(
-        (rsp, throwable) -> {
-          if (rsp != null) {
-            ssr.nl = rsp.getResponse();
-            srsp.setShardAddress(rsp.getServer());
-            ssr.elapsedTime =
-                TimeUnit.MILLISECONDS.convert(System.nanoTime() - startTime, 
TimeUnit.NANOSECONDS);
-            responses.add(transfomResponse(sreq, srsp, shard));
-          } else if (throwable != null) {
-            ssr.elapsedTime =
-                TimeUnit.MILLISECONDS.convert(System.nanoTime() - startTime, 
TimeUnit.NANOSECONDS);
-            srsp.setException(throwable);
-            if (throwable instanceof SolrException) {
-              srsp.setResponseCode(((SolrException) throwable).code());
-            }
-            responses.add(transfomResponse(sreq, srsp, shard));
-          }
-        });
+    makeShardRequest(sreq, shard, params, lbReq, ssr, srsp, startTimeNS);
+  }
 
-    responseFutureMap.put(srsp, future);
+  /**
+   * Do the actual work of sending a request to a shard and receiving the 
response
+   *
+   * @param sreq the request to make
+   * @param shard the shard to address
+   * @param params request parameters
+   * @param lbReq the load balanced request suitable for LBHttp2SolrClient
+   * @param ssr the response collector part 1
+   * @param srsp the shard response collector
+   * @param startTimeNS the time at which the request was initiated, likely 
just prior to calling
+   *     this method.
+   */
+  // future work might see if we can reduce this parameter list. For example,
+  // why do we need 2 separate response objects?
+  protected void makeShardRequest(
+      ShardRequest sreq,
+      String shard,
+      ModifiableSolrParams params,
+      LBSolrClient.Req lbReq,
+      SimpleSolrResponse ssr,
+      ShardResponse srsp,
+      long startTimeNS) {
+    CompletableFuture<LBSolrClient.Rsp> future = 
this.lbClient.requestAsync(lbReq);
+    future.whenComplete(new ShardRequestCallback(ssr, srsp, startTimeNS, sreq, 
shard, params));
+    synchronized (FUTURE_MAP_LOCK) {
+      // we want to ensure that there is a future in flight before incrementing
+      // pending. If anything fails such that a request/future is not created 
there is
+      // potential for the request to hang forever waiting on a 
responses.take()
+      // and so if anything failed during future creation we would get stuck.
+      pending.incrementAndGet();
+      responseFutureMap.put(srsp, future);
+    }
   }
 
   /** Subclasses could modify the request based on the shard */
-  protected QueryRequest makeQueryRequest(
+  @SuppressWarnings("unused")
+  protected QueryRequest createQueryRequest(
       final ShardRequest sreq, ModifiableSolrParams params, String shard) {
     // use generic request to avoid extra processing of queries
     return new QueryRequest(params);
   }
 
   /** Subclasses could modify the Response based on the shard */
-  protected ShardResponse transfomResponse(
+  @SuppressWarnings("unused")
+  protected ShardResponse transformResponse(
       final ShardRequest sreq, ShardResponse rsp, String shard) {
     return rsp;
   }
@@ -229,11 +319,19 @@ public class HttpShardHandler extends ShardHandler {
 
   private ShardResponse take(boolean bailOnError) {
     try {
-      while (pending.get() > 0) {
-        ShardResponse rsp = responses.take();
-        responseFutureMap.remove(rsp);
+      while (responsesPending()) {
+        ShardResponse rsp;
+        synchronized (FUTURE_MAP_LOCK) {
+          // in the parallel case we need to recheck responsesPending()
+          // in case all attempts to submit failed.
+          rsp = responses.poll(50, TimeUnit.MILLISECONDS);
+          if (rsp == null) {
+            continue;
+          }
+          responseFutureMap.remove(rsp);
+          pending.decrementAndGet();
+        }
 
-        pending.decrementAndGet();
         if (bailOnError && rsp.getException() != null)
           return rsp; // if exception, return immediately
         // add response to the response list... we do this after the take() and
@@ -241,6 +339,7 @@ public class HttpShardHandler extends ShardHandler {
         // for a request was received.  Otherwise we might return the same
         // request more than once.
         rsp.getShardRequest().responses.add(rsp);
+
         if (rsp.getShardRequest().responses.size() == 
rsp.getShardRequest().actualShards.length) {
           return rsp;
         }
@@ -251,13 +350,19 @@ public class HttpShardHandler extends ShardHandler {
     return null;
   }
 
+  protected boolean responsesPending() {
+    return pending.get() > 0;
+  }
+
   @Override
   public void cancelAll() {
-    for (CompletableFuture<LBSolrClient.Rsp> future : 
responseFutureMap.values()) {
-      future.cancel(true);
-      pending.decrementAndGet();
+    synchronized (FUTURE_MAP_LOCK) {
+      for (CompletableFuture<LBSolrClient.Rsp> future : 
responseFutureMap.values()) {
+        future.cancel(true);
+        pending.decrementAndGet();
+      }
+      responseFutureMap.clear();
     }
-    responseFutureMap.clear();
   }
 
   @Override
@@ -312,7 +417,7 @@ public class HttpShardHandler extends ShardHandler {
         // be an optimization?
       }
 
-      if (!ShardParams.getShardsTolerantAsBool(params)) {
+      if (!getShardsTolerantAsBool(req)) {
         for (int i = 0; i < rb.slices.length; i++) {
           if (replicaSource.getReplicasBySlice(i).isEmpty()) {
             final ReplicaSource allActiveReplicaSource =
@@ -395,4 +500,51 @@ public class HttpShardHandler extends ShardHandler {
   public ShardHandlerFactory getShardHandlerFactory() {
     return httpShardHandlerFactory;
   }
+
+  class ShardRequestCallback implements BiConsumer<LBSolrClient.Rsp, 
Throwable> {
+    private final SimpleSolrResponse ssr;
+    private final ShardResponse srsp;
+    private final long startTimeNS;
+    private final ShardRequest sreq;
+    private final String shard;
+    private final ModifiableSolrParams params;
+
+    public ShardRequestCallback(
+        SimpleSolrResponse ssr,
+        ShardResponse srsp,
+        long startTimeNS,
+        ShardRequest sreq,
+        String shard,
+        ModifiableSolrParams params) {
+      this.ssr = ssr;
+      this.srsp = srsp;
+      this.startTimeNS = startTimeNS;
+      this.sreq = sreq;
+      this.shard = shard;
+      this.params = params;
+    }
+
+    @Override
+    public void accept(LBSolrClient.Rsp rsp, Throwable throwable) {
+      if (rsp != null) {
+        ssr.nl = rsp.getResponse();
+        srsp.setShardAddress(rsp.getServer());
+        ssr.elapsedTime =
+            TimeUnit.MILLISECONDS.convert(System.nanoTime() - startTimeNS, 
TimeUnit.NANOSECONDS);
+        responses.add(HttpShardHandler.this.transformResponse(sreq, srsp, 
shard));
+      } else if (throwable != null) {
+        ssr.elapsedTime =
+            TimeUnit.MILLISECONDS.convert(System.nanoTime() - startTimeNS, 
TimeUnit.NANOSECONDS);
+        srsp.setException(throwable);
+        if (throwable instanceof SolrException) {
+          srsp.setResponseCode(((SolrException) throwable).code());
+        }
+        responses.add(HttpShardHandler.this.transformResponse(sreq, srsp, 
shard));
+        if (disallowPartialResults(params)) {
+          HttpShardHandler.this
+              .cancelAll(); // Note: method synchronizes 
RESPONSE_CANCELABLE_LOCK on entry
+        }
+      }
+    }
+  }
 }
diff --git 
a/solr/core/src/java/org/apache/solr/handler/component/ParallelHttpShardHandler.java
 
b/solr/core/src/java/org/apache/solr/handler/component/ParallelHttpShardHandler.java
index 4a8dd5e3267..7d6cf13874f 100644
--- 
a/solr/core/src/java/org/apache/solr/handler/component/ParallelHttpShardHandler.java
+++ 
b/solr/core/src/java/org/apache/solr/handler/component/ParallelHttpShardHandler.java
@@ -17,13 +17,11 @@
 package org.apache.solr.handler.component;
 
 import java.lang.invoke.MethodHandles;
-import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
 import net.jcip.annotations.NotThreadSafe;
 import org.apache.solr.client.solrj.impl.LBSolrClient;
-import org.apache.solr.common.SolrException;
 import org.apache.solr.common.params.ModifiableSolrParams;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -41,57 +39,78 @@ import org.slf4j.LoggerFactory;
 @NotThreadSafe
 public class ParallelHttpShardHandler extends HttpShardHandler {
 
+  @SuppressWarnings("unused")
   private static final Logger log = 
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
   private final ExecutorService commExecutor;
 
+  /*
+   * Unlike the basic HttpShardHandler, this class allows us to exit submit 
before
+   * pending is incremented and the responseFutureMap is updated. If the 
runnables that
+   * do that are slow to execute the calling code could attempt to 
takeCompleted(),
+   * while pending is still zero. In this condition, the code would assume 
that all
+   * requests are processed (despite the runnables created by this class still
+   * waiting). Thus, we need to track that there are attempts still in flight.
+   *
+   * This tracking is complicated by the fact that there could be a failure in 
the
+   * runnable that causes the request to never be created and pending to never 
be
+   * incremented. Thus, we need to know that we have attempted something AND 
that that
+   * attempt has also been processed by the executor.
+   *
+   * This condition is added to the check that controls the loop in take via 
the
+   * override for #responsesPending(). We rely on calling code call submit for 
all
+   * requests desired before the call to takeCompleted()
+   */
+  AtomicInteger attemptStart = new AtomicInteger(0);
+  AtomicInteger attemptCount = new AtomicInteger(0);
+
   public ParallelHttpShardHandler(ParallelHttpShardHandlerFactory 
httpShardHandlerFactory) {
     super(httpShardHandlerFactory);
     this.commExecutor = httpShardHandlerFactory.commExecutor;
   }
 
   @Override
-  public void submit(ShardRequest sreq, String shard, ModifiableSolrParams 
params) {
-    // do this outside of the callable for thread safety reasons
-    final List<String> urls = getURLs(shard);
-    final var lbReq = prepareLBRequest(sreq, shard, params, urls);
-    final var srsp = prepareShardResponse(sreq, shard);
-    final var ssr = new SimpleSolrResponse();
-    srsp.setSolrResponse(ssr);
-    pending.incrementAndGet();
-
-    if (urls.isEmpty()) {
-      recordNoUrlShardResponse(srsp, shard);
-      return;
-    }
+  protected boolean responsesPending() {
+    // ensure we can't exit while loop in HttpShardHandler.take(boolean) until 
we've completed
+    // as many Runnable actions as we created.
+    return super.responsesPending() || attemptStart.get() > attemptCount.get();
+  }
 
-    long startTime = System.nanoTime();
+  @Override
+  protected void makeShardRequest(
+      ShardRequest sreq,
+      String shard,
+      ModifiableSolrParams params,
+      LBSolrClient.Req lbReq,
+      SimpleSolrResponse ssr,
+      ShardResponse srsp,
+      long startTimeNS) {
     final Runnable executeRequestRunnable =
         () -> {
-          CompletableFuture<LBSolrClient.Rsp> future = 
this.lbClient.requestAsync(lbReq);
-          future.whenComplete(
-              (rsp, throwable) -> {
-                if (rsp != null) {
-                  ssr.nl = rsp.getResponse();
-                  srsp.setShardAddress(rsp.getServer());
-                  ssr.elapsedTime =
-                      TimeUnit.MILLISECONDS.convert(
-                          System.nanoTime() - startTime, TimeUnit.NANOSECONDS);
-                  responses.add(srsp);
-                } else if (throwable != null) {
-                  ssr.elapsedTime =
-                      TimeUnit.MILLISECONDS.convert(
-                          System.nanoTime() - startTime, TimeUnit.NANOSECONDS);
-                  srsp.setException(throwable);
-                  if (throwable instanceof SolrException) {
-                    srsp.setResponseCode(((SolrException) throwable).code());
-                  }
-                  responses.add(srsp);
-                }
-              });
-          responseFutureMap.put(srsp, future);
+          try {
+            CompletableFuture<LBSolrClient.Rsp> future = 
this.lbClient.requestAsync(lbReq);
+            future.whenComplete(
+                new ShardRequestCallback(ssr, srsp, startTimeNS, sreq, shard, 
params));
+            synchronized (FUTURE_MAP_LOCK) {
+              // we want to ensure that there is a future in flight before 
incrementing
+              // pending, because there is a risk that the  request will hang 
forever waiting
+              // on a responses.take() in HttpShardHandler.take(boolean) if 
anything failed
+              // during future creation. It is not a problem if the response 
shows up before
+              // we increment pending. The attemptingSubmit flag guards us 
against inadvertently
+              // skipping the while loop in HttpShardHandler.take(boolean) 
until at least
+              // one runnable has been executed.
+              pending.incrementAndGet();
+              responseFutureMap.put(srsp, future);
+            }
+          } finally {
+            // it must not be possible to exit the runnable in any way without 
calling this.
+            attemptCount.incrementAndGet();
+          }
         };
 
+    // not clear how errors emanating from requestAsync or the whenComplete() 
callback
+    // are to propagated out of the runnable?
+    attemptStart.incrementAndGet();
     CompletableFuture.runAsync(executeRequestRunnable, commExecutor);
   }
 }
diff --git 
a/solr/core/src/java/org/apache/solr/handler/component/QueryComponent.java 
b/solr/core/src/java/org/apache/solr/handler/component/QueryComponent.java
index 3c558feffc3..3acb78ed78f 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/QueryComponent.java
+++ b/solr/core/src/java/org/apache/solr/handler/component/QueryComponent.java
@@ -17,6 +17,7 @@
 package org.apache.solr.handler.component;
 
 import static org.apache.solr.common.params.CommonParams.QUERY_UUID;
+import static org.apache.solr.response.SolrQueryResponse.haveCompleteResults;
 
 import java.io.IOException;
 import java.io.PrintWriter;
@@ -1338,10 +1339,8 @@ public class QueryComponent extends SearchComponent {
                   (NamedList<?>)
                       SolrResponseUtil.getSubsectionFromShardResponse(
                           rb, srsp, "responseHeader", false));
-          if (Boolean.TRUE.equals(
-              responseHeader.getBooleanArg(
-                  SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY))) {
-            rb.rsp.setPartialResults();
+          if (!haveCompleteResults(responseHeader)) { // partial or omitted 
partials
+            rb.rsp.setPartialResults(rb.req);
             rb.rsp.addPartialResponseDetail(
                 
responseHeader.get(SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_DETAILS_KEY));
           }
diff --git 
a/solr/core/src/java/org/apache/solr/handler/component/ResponseBuilder.java 
b/solr/core/src/java/org/apache/solr/handler/component/ResponseBuilder.java
index 040ba5aa678..80d0b726ff7 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/ResponseBuilder.java
+++ b/solr/core/src/java/org/apache/solr/handler/component/ResponseBuilder.java
@@ -447,8 +447,10 @@ public class ResponseBuilder {
   /** Sets results from a SolrIndexSearcher.QueryResult. */
   public void setResult(QueryResult result) {
     setResults(result.getDocListAndSet());
+    if (result.isPartialResultOmitted() || result.isPartialResults()) {
+      rsp.setPartialResults(req);
+    }
     if (result.isPartialResults()) {
-      rsp.setPartialResults();
       if (getResults() != null && getResults().docList == null) {
         getResults().docList =
             new DocSlice(0, 0, new int[] {}, new float[] {}, 0, 0, 
TotalHits.Relation.EQUAL_TO);
diff --git 
a/solr/core/src/java/org/apache/solr/handler/component/SearchHandler.java 
b/solr/core/src/java/org/apache/solr/handler/component/SearchHandler.java
index 2996992b1dd..e70b286e9f3 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/SearchHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/component/SearchHandler.java
@@ -487,7 +487,7 @@ public class SearchHandler extends RequestHandlerBase
           debug.add("explain", new NamedList<>());
           rb.rsp.add("debug", debug);
         }
-        rb.rsp.setPartialResults();
+        rb.rsp.setPartialResults(rb.req);
       }
     } else {
       // a distributed request
@@ -551,7 +551,7 @@ public class SearchHandler extends RequestHandlerBase
           // now wait for replies, but if anyone puts more requests on
           // the outgoing queue, send them out immediately (by exiting
           // this loop)
-          boolean tolerant = 
ShardParams.getShardsTolerantAsBool(rb.req.getParams());
+          boolean tolerant = HttpShardHandler.getShardsTolerantAsBool(rb.req);
           while (rb.outgoing.size() == 0) {
             ShardResponse srsp =
                 tolerant
@@ -559,6 +559,20 @@ public class SearchHandler extends RequestHandlerBase
                     : shardHandler1.takeCompletedOrError();
             if (srsp == null) break; // no more requests to wait for
 
+            boolean anyResponsesPartial =
+                srsp.getShardRequest().responses.stream()
+                    .anyMatch(
+                        response -> {
+                          NamedList<Object> resp = 
response.getSolrResponse().getResponse();
+                          if (resp == null) {
+                            return false;
+                          }
+                          Object recursive = 
resp.findRecursive("responseHeader", "partialResults");
+                          return recursive != null;
+                        });
+            if (anyResponsesPartial) {
+              rsp.setPartialResults(rb.req);
+            }
             // Was there an exception?
             if (srsp.getException() != null) {
               // If things are not tolerant, abort everything and rethrow
@@ -579,7 +593,7 @@ public class SearchHandler extends RequestHandlerBase
                 if (allShardsFailed) {
                   throwSolrException(srsp.getException());
                 } else {
-                  rsp.setPartialResults();
+                  rsp.setPartialResults(rb.req);
                 }
               }
             }
diff --git 
a/solr/core/src/java/org/apache/solr/handler/component/ShardHandler.java 
b/solr/core/src/java/org/apache/solr/handler/component/ShardHandler.java
index 2717bc47845..1246b2e2484 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/ShardHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/component/ShardHandler.java
@@ -55,8 +55,9 @@ public abstract class ShardHandler {
   public abstract void submit(ShardRequest sreq, String shard, 
ModifiableSolrParams params);
 
   /**
-   * returns a ShardResponse of the last response correlated with a 
ShardRequest. This won't return
-   * early if it runs into an error.
+   * Returns a ShardResponse of the last response correlated with a 
ShardRequest. This won't return
+   * early if it runs into an error. Callers are responsible for ensuring that 
this can't be called
+   * before requests have been submitted with submit.
    */
   public abstract ShardResponse takeCompletedIncludingErrors();
 
@@ -64,8 +65,9 @@ public abstract class ShardHandler {
   // distinguish between different ShardRequest objects as it seems to 
advertise? What's going on
   // here?
   /**
-   * returns a ShardResponse of the last response correlated with a 
ShardRequest, or immediately
-   * returns a ShardResponse if there was an error detected
+   * Returns a ShardResponse of the last response correlated with a 
ShardRequest, or immediately
+   * returns a ShardResponse if there was an error detected. Callers are 
responsible for ensuring
+   * that this can't be called before requests have been submitted with submit.
    */
   public abstract ShardResponse takeCompletedOrError();
 
diff --git 
a/solr/core/src/java/org/apache/solr/handler/component/ShardResponse.java 
b/solr/core/src/java/org/apache/solr/handler/component/ShardResponse.java
index dee5fcdb803..289a6e94182 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/ShardResponse.java
+++ b/solr/core/src/java/org/apache/solr/handler/component/ShardResponse.java
@@ -19,11 +19,16 @@ package org.apache.solr.handler.component;
 import org.apache.solr.client.solrj.SolrResponse;
 
 public final class ShardResponse {
+
+  // WARNING: this class is used as a key in a map and is modified during that 
period.
+  // Do not implement equals() or hashCode() without considering impact on
+  // HttpShardHandler.responseFutureMap
+
   private ShardRequest req;
   private String shard;
   private String nodeName;
   private volatile String shardAddress; // the specific shard that this 
response was received from
-  private int rspCode;
+  private volatile int rspCode;
   private volatile Throwable exception;
   private SolrResponse rsp;
 
diff --git a/solr/core/src/java/org/apache/solr/request/SolrQueryRequest.java 
b/solr/core/src/java/org/apache/solr/request/SolrQueryRequest.java
index 613ae31d438..0d2c7463b16 100644
--- a/solr/core/src/java/org/apache/solr/request/SolrQueryRequest.java
+++ b/solr/core/src/java/org/apache/solr/request/SolrQueryRequest.java
@@ -25,9 +25,11 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import org.apache.solr.cloud.CloudDescriptor;
+import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.util.CommandOperation;
 import org.apache.solr.common.util.ContentStream;
+import org.apache.solr.common.util.EnvUtils;
 import org.apache.solr.core.CoreContainer;
 import org.apache.solr.core.SolrCore;
 import org.apache.solr.schema.IndexSchema;
@@ -42,6 +44,39 @@ import org.apache.solr.util.RTimerTree;
  */
 public interface SolrQueryRequest extends AutoCloseable {
 
+  /** This is the system property for {@link #ALLOW_PARTIAL_RESULTS_DEFAULT} */
+  String SOLR_ALLOW_PARTIAL_RESULTS_DEFAULT = 
"solr.allowPartialResultsDefault";
+
+  // silly getBoolean doesn't take a default.
+  /**
+   * Users can set {@link SolrQueryRequest#SOLR_ALLOW_PARTIAL_RESULTS_DEFAULT} 
system property to
+   * true, and solr will omit results when any shard fails due query execution 
limits (time, cpu
+   * etc.). By default, this is set to true. Setting it to false will reduce 
processing, cpu and
+   * network associated with collecting and transmitting partial results. This 
setting can be
+   * overridden (in either direction) on a per-request basis with {@code
+   * &allowPartialResults=[true|false]}. When results have been omitted the 
response header should
+   * contain a partialResults element with the value "omitted"
+   */
+  boolean ALLOW_PARTIAL_RESULTS_DEFAULT =
+      EnvUtils.getPropertyAsBool(SOLR_ALLOW_PARTIAL_RESULTS_DEFAULT, true);
+
+  /**
+   * Tests if the partials for the request should be discarded. Examines {@link
+   * SolrQueryRequest#ALLOW_PARTIAL_RESULTS_DEFAULT} system property and also 
examines {@link
+   * CommonParams#PARTIAL_RESULTS} request param. The Request Parameter takes 
precedence if both are
+   * set.
+   *
+   * @return true if partials should be discarded.
+   * @param params the request parameters
+   */
+  static boolean allowPartialResults(SolrParams params) {
+    return params.getBool(CommonParams.PARTIAL_RESULTS, 
ALLOW_PARTIAL_RESULTS_DEFAULT);
+  }
+
+  static boolean disallowPartialResults(SolrParams params) {
+    return !allowPartialResults(params);
+  }
+
   /** returns the current request parameters */
   SolrParams getParams();
 
diff --git a/solr/core/src/java/org/apache/solr/request/SolrRequestInfo.java 
b/solr/core/src/java/org/apache/solr/request/SolrRequestInfo.java
index 193b838e9cd..61b51a6af20 100644
--- a/solr/core/src/java/org/apache/solr/request/SolrRequestInfo.java
+++ b/solr/core/src/java/org/apache/solr/request/SolrRequestInfo.java
@@ -24,6 +24,7 @@ import java.util.ArrayList;
 import java.util.Date;
 import java.util.Deque;
 import java.util.List;
+import java.util.Optional;
 import java.util.TimeZone;
 import java.util.concurrent.atomic.AtomicReference;
 import javax.servlet.http.HttpServletRequest;
@@ -66,6 +67,14 @@ public class SolrRequestInfo {
     return stack.peek();
   }
 
+  public static Optional<SolrRequestInfo> getReqInfo() {
+    return Optional.ofNullable(getRequestInfo());
+  }
+
+  public static Optional<SolrQueryRequest> getRequest() {
+    return getReqInfo().map(i -> i.req);
+  }
+
   /**
    * Adds the SolrRequestInfo onto a stack held in a {@link ThreadLocal}. 
Remember to call {@link
    * #clearRequestInfo()}!
diff --git a/solr/core/src/java/org/apache/solr/response/SolrQueryResponse.java 
b/solr/core/src/java/org/apache/solr/response/SolrQueryResponse.java
index 2f952649fd0..b399ce57b79 100644
--- a/solr/core/src/java/org/apache/solr/response/SolrQueryResponse.java
+++ b/solr/core/src/java/org/apache/solr/response/SolrQueryResponse.java
@@ -16,15 +16,18 @@
  */
 package org.apache.solr.response;
 
+import static org.apache.solr.request.SolrQueryRequest.disallowPartialResults;
+
 import java.util.Collection;
 import java.util.Date;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.Map.Entry;
 import javax.servlet.http.HttpServletResponse;
-import org.apache.solr.common.params.CommonParams;
+import org.apache.solr.common.SolrException;
 import org.apache.solr.common.util.NamedList;
 import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.search.ReturnFields;
 import org.apache.solr.search.SolrReturnFields;
 
@@ -61,7 +64,7 @@ import org.apache.solr.search.SolrReturnFields;
  */
 public class SolrQueryResponse {
   public static final String NAME = "response";
-  public static final String RESPONSE_HEADER_PARTIAL_RESULTS_KEY = 
CommonParams.PARTIAL_RESULTS;
+  public static final String RESPONSE_HEADER_PARTIAL_RESULTS_KEY = 
"partialResults";
   public static final String RESPONSE_HEADER_PARTIAL_RESULTS_DETAILS_KEY = 
"partialResultsDetails";
   public static final String RESPONSE_HEADER_SEGMENT_TERMINATED_EARLY_KEY =
       "segmentTerminatedEarly";
@@ -139,30 +142,63 @@ public class SolrQueryResponse {
 
   /**
    * If {@link #getResponseHeader()} is available, set {@link 
#RESPONSE_HEADER_PARTIAL_RESULTS_KEY}
-   * flag to true.
+   * attribute to true or "omitted" as required.
    */
-  public void setPartialResults() {
+  public void setPartialResults(SolrQueryRequest req) {
     NamedList<Object> header = getResponseHeader();
-    if (header != null
-        && header.get(SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY) 
== null) {
-      header.add(SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY, 
Boolean.TRUE);
+    if (header != null) {
+      if (header.get(SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY) == 
null) {
+        Object value = 
partialResultsStatus(disallowPartialResults(req.getParams()));
+        header.add(SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY, 
value);
+      }
     }
   }
 
+  public static Object partialResultsStatus(boolean discarding) {
+    return discarding ? "omitted" : Boolean.TRUE;
+  }
+
   /**
    * If {@link #getResponseHeader()} is available, return the value of {@link
    * #RESPONSE_HEADER_PARTIAL_RESULTS_KEY} or false.
+   *
+   * @param header the response header
+   * @return true if there are results, but they do not represent the full 
results, false if the
+   *     results are complete, or intentionally omitted
    */
-  public boolean isPartialResults() {
-    NamedList<Object> header = getResponseHeader();
+  public static boolean isPartialResults(NamedList<?> header) {
     if (header != null) {
-      return Boolean.TRUE.equals(
-          
header.getBooleanArg(SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY));
+      // actual value may be true/false/omitted
+      return "true"
+          
.equalsIgnoreCase(String.valueOf(header.get(RESPONSE_HEADER_PARTIAL_RESULTS_KEY)));
     } else {
       return false;
     }
   }
 
+  /**
+   * Test if the entire results have been returned, or if some form of 
limit/cancel/tolerant logic
+   * has returned incomplete (or possibly empty) results.
+   *
+   * @param header the response header
+   * @return true if full results are returning normally false otherwise.
+   */
+  public static boolean haveCompleteResults(NamedList<?> header) {
+    if (header == null) {
+      // partial/omitted results will have placed something in the header, so 
it should exist.
+      return true;
+    }
+    // "true" and "omitted" should both respond with false
+    Object o = header.get(RESPONSE_HEADER_PARTIAL_RESULTS_KEY);
+    // putting this check here so that if anything new is added we don't 
forget to consider the
+    // effect on code that calls this function. Could contain either 
Boolean.TRUE or "omitted"
+    if (o != null && !(Boolean.TRUE.equals(o) || "omitted".equals(o))) {
+      throw new SolrException(
+          SolrException.ErrorCode.SERVER_ERROR, "Unrecognized value for 
partialResults:" + o);
+    }
+    return o == null;
+  }
+
   /**
    * If {@link #getResponseHeader()} is available, add a reason for returning 
partial response.
    *
diff --git a/solr/core/src/java/org/apache/solr/search/QueryLimits.java 
b/solr/core/src/java/org/apache/solr/search/QueryLimits.java
index c2bd205c41d..7aee5657678 100644
--- a/solr/core/src/java/org/apache/solr/search/QueryLimits.java
+++ b/solr/core/src/java/org/apache/solr/search/QueryLimits.java
@@ -23,6 +23,7 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
 import org.apache.lucene.index.QueryTimeout;
+import org.apache.solr.common.SolrException;
 import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.request.SolrRequestInfo;
@@ -35,6 +36,7 @@ import org.apache.solr.util.TestInjection;
  * return true the next time it is checked (it may be checked in either Lucene 
code or Solr code)
  */
 public class QueryLimits implements QueryTimeout {
+  public static final String UNLIMITED = "This request is unlimited.";
   private final List<QueryLimit> limits =
       new ArrayList<>(3); // timeAllowed, cpu, and memory anticipated
 
@@ -59,8 +61,7 @@ public class QueryLimits implements QueryTimeout {
    */
   public QueryLimits(SolrQueryRequest req, SolrQueryResponse rsp) {
     this.rsp = rsp;
-    this.allowPartialResults =
-        req != null ? req.getParams().getBool(CommonParams.PARTIAL_RESULTS, 
true) : true;
+    this.allowPartialResults = req == null || 
SolrQueryRequest.allowPartialResults(req.getParams());
     if (req != null) {
       if (hasTimeLimit(req)) {
         limits.add(new TimeAllowedLimit(req));
@@ -109,14 +110,20 @@ public class QueryLimits implements QueryTimeout {
    *
    * @param label optional label to indicate the caller.
    * @return true if the caller should stop processing and return partial 
results, false otherwise.
-   * @throws QueryLimitsExceededException if {@link 
CommonParams#PARTIAL_RESULTS} request parameter
-   *     is false and limits have been reached.
+   * @throws QueryLimitsExceededException if {@link #allowPartialResults} is 
false and limits have
+   *     been reached.
    */
   public boolean maybeExitWithPartialResults(String label) throws 
QueryLimitsExceededException {
     if (isLimitsEnabled() && shouldExit()) {
       if (allowPartialResults) {
         if (rsp != null) {
-          rsp.setPartialResults();
+          SolrRequestInfo requestInfo = SolrRequestInfo.getRequestInfo();
+          if (requestInfo == null) {
+            throw new SolrException(
+                SolrException.ErrorCode.SERVER_ERROR,
+                "No request active, but attempting to exit with partial 
results?");
+          }
+          rsp.setPartialResults(requestInfo.getReq());
           rsp.addPartialResponseDetail(formatExceptionMessage(label));
         }
         return true;
@@ -140,7 +147,7 @@ public class QueryLimits implements QueryTimeout {
    */
   public String limitStatusMessage() {
     if (limits.isEmpty()) {
-      return "This request is unlimited.";
+      return UNLIMITED;
     }
     StringBuilder sb = new StringBuilder("Query limits: ");
     for (QueryTimeout limit : limits) {
diff --git a/solr/core/src/java/org/apache/solr/search/QueryResult.java 
b/solr/core/src/java/org/apache/solr/search/QueryResult.java
index 9c9a8e6f60d..900a72214e7 100755
--- a/solr/core/src/java/org/apache/solr/search/QueryResult.java
+++ b/solr/core/src/java/org/apache/solr/search/QueryResult.java
@@ -19,7 +19,8 @@ package org.apache.solr.search;
 /** The result of a search. */
 public class QueryResult {
 
-  private boolean partialResults;
+  // Object for back compatibility so that we render true not "true" in json
+  private Object partialResults;
   private Boolean segmentTerminatedEarly;
   private DocListAndSet docListAndSet;
   private CursorMark nextCursorMark;
@@ -49,10 +50,15 @@ public class QueryResult {
   }
 
   public boolean isPartialResults() {
-    return partialResults;
+    // omitted is equivalent to false/empty for java logic
+    return Boolean.parseBoolean(String.valueOf(partialResults));
   }
 
-  public void setPartialResults(boolean partialResults) {
+  public boolean isPartialResultOmitted() {
+    return "omitted".equals(partialResults);
+  }
+
+  public void setPartialResults(Object partialResults) {
     this.partialResults = partialResults;
   }
 
diff --git a/solr/core/src/java/org/apache/solr/search/facet/FacetModule.java 
b/solr/core/src/java/org/apache/solr/search/facet/FacetModule.java
index 5a84339a775..ae9ee159791 100644
--- a/solr/core/src/java/org/apache/solr/search/facet/FacetModule.java
+++ b/solr/core/src/java/org/apache/solr/search/facet/FacetModule.java
@@ -17,6 +17,7 @@
 package org.apache.solr.search.facet;
 
 import static org.apache.solr.common.util.Utils.fromJSONString;
+import static org.apache.solr.response.SolrQueryResponse.haveCompleteResults;
 
 import java.io.IOException;
 import java.util.Collection;
@@ -37,7 +38,6 @@ import org.apache.solr.handler.component.ResponseBuilder;
 import org.apache.solr.handler.component.SearchComponent;
 import org.apache.solr.handler.component.ShardRequest;
 import org.apache.solr.handler.component.ShardResponse;
-import org.apache.solr.response.SolrQueryResponse;
 import org.apache.solr.search.QueryContext;
 import org.noggit.CharArr;
 import org.noggit.JSONWriter;
@@ -300,10 +300,8 @@ public class FacetModule extends SearchComponent {
       if (facet == null) {
         SimpleOrderedMap<?> shardResponseHeader =
             (SimpleOrderedMap<?>) rsp.getResponse().get("responseHeader");
-        if (Boolean.TRUE.equals(
-            shardResponseHeader.getBooleanArg(
-                SolrQueryResponse.RESPONSE_HEADER_PARTIAL_RESULTS_KEY))) {
-          rb.rsp.setPartialResults();
+        if (!haveCompleteResults(shardResponseHeader)) {
+          rb.rsp.setPartialResults(rb.req);
         }
         continue;
       }
diff --git 
a/solr/core/src/java/org/apache/solr/search/grouping/CommandHandler.java 
b/solr/core/src/java/org/apache/solr/search/grouping/CommandHandler.java
index bff8cb1c0b4..b444e7b77cf 100644
--- a/solr/core/src/java/org/apache/solr/search/grouping/CommandHandler.java
+++ b/solr/core/src/java/org/apache/solr/search/grouping/CommandHandler.java
@@ -16,6 +16,8 @@
  */
 package org.apache.solr.search.grouping;
 
+import static org.apache.solr.response.SolrQueryResponse.partialResultsStatus;
+
 import java.io.IOException;
 import java.lang.invoke.MethodHandles;
 import java.util.ArrayList;
@@ -34,6 +36,8 @@ import 
org.apache.lucene.search.grouping.AllGroupHeadsCollector;
 import org.apache.lucene.search.grouping.TermGroupSelector;
 import org.apache.lucene.search.grouping.ValueSourceGroupSelector;
 import org.apache.solr.common.util.NamedList;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.request.SolrRequestInfo;
 import org.apache.solr.schema.FieldType;
 import org.apache.solr.schema.SchemaField;
 import org.apache.solr.search.BitDocSet;
@@ -122,7 +126,6 @@ public class CommandHandler {
   private final boolean needDocSet;
   private final boolean truncateGroups;
   private final boolean includeHitCount;
-  private boolean partialResults = false;
   private int totalHitCount;
 
   private DocSet docSet;
@@ -225,7 +228,15 @@ public class CommandHandler {
     if (docSet != null) {
       queryResult.setDocSet(docSet);
     }
-    queryResult.setPartialResults(partialResults);
+    if (queryResult.isPartialResults()) {
+      queryResult.setPartialResults(
+          partialResultsStatus(
+              SolrRequestInfo.getRequest()
+                  .map(
+                      solrQueryRequest ->
+                          
SolrQueryRequest.disallowPartialResults(solrQueryRequest.getParams()))
+                  .orElse(false)));
+    }
     return transformer.transform(commands);
   }
 
@@ -257,7 +268,6 @@ public class CommandHandler {
       searcher.search(query, collector);
     } catch (TimeLimitingCollector.TimeExceededException
         | ExitableDirectoryReader.ExitingReaderException x) {
-      partialResults = true;
       log.warn("Query: {}; ", query, x);
     }
 
diff --git 
a/solr/core/src/java/org/apache/solr/search/grouping/distributed/responseprocessor/SearchGroupShardResponseProcessor.java
 
b/solr/core/src/java/org/apache/solr/search/grouping/distributed/responseprocessor/SearchGroupShardResponseProcessor.java
index 35fcf5e4b21..e9af407a3a8 100644
--- 
a/solr/core/src/java/org/apache/solr/search/grouping/distributed/responseprocessor/SearchGroupShardResponseProcessor.java
+++ 
b/solr/core/src/java/org/apache/solr/search/grouping/distributed/responseprocessor/SearchGroupShardResponseProcessor.java
@@ -33,6 +33,7 @@ import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.common.params.ShardParams;
 import org.apache.solr.common.util.NamedList;
 import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.handler.component.HttpShardHandler;
 import org.apache.solr.handler.component.ResponseBuilder;
 import org.apache.solr.handler.component.ShardRequest;
 import org.apache.solr.handler.component.ShardResponse;
@@ -104,8 +105,8 @@ public class SearchGroupShardResponseProcessor implements 
ShardResponseProcessor
         }
         shardInfo.add(srsp.getShard(), nl);
       }
-      if (ShardParams.getShardsTolerantAsBool(rb.req.getParams()) && 
srsp.getException() != null) {
-        rb.rsp.setPartialResults();
+      if (HttpShardHandler.getShardsTolerantAsBool(rb.req) && 
srsp.getException() != null) {
+        rb.rsp.setPartialResults(rb.req);
         continue; // continue if there was an error and we're tolerant.
       }
       maxElapsedTime = Math.max(maxElapsedTime, solrResponse.getElapsedTime());
diff --git 
a/solr/core/src/java/org/apache/solr/search/grouping/distributed/responseprocessor/TopGroupsShardResponseProcessor.java
 
b/solr/core/src/java/org/apache/solr/search/grouping/distributed/responseprocessor/TopGroupsShardResponseProcessor.java
index 0a2eea9b9ee..206fd982964 100644
--- 
a/solr/core/src/java/org/apache/solr/search/grouping/distributed/responseprocessor/TopGroupsShardResponseProcessor.java
+++ 
b/solr/core/src/java/org/apache/solr/search/grouping/distributed/responseprocessor/TopGroupsShardResponseProcessor.java
@@ -34,6 +34,7 @@ import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.common.params.ShardParams;
 import org.apache.solr.common.util.NamedList;
 import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.handler.component.HttpShardHandler;
 import org.apache.solr.handler.component.ResponseBuilder;
 import org.apache.solr.handler.component.ShardDoc;
 import org.apache.solr.handler.component.ShardRequest;
@@ -116,8 +117,8 @@ public class TopGroupsShardResponseProcessor implements 
ShardResponseProcessor {
         }
         shardInfo.add(srsp.getShard(), individualShardInfo);
       }
-      if (ShardParams.getShardsTolerantAsBool(rb.req.getParams()) && 
srsp.getException() != null) {
-        rb.rsp.setPartialResults();
+      if (HttpShardHandler.getShardsTolerantAsBool(rb.req) && 
srsp.getException() != null) {
+        rb.rsp.setPartialResults(rb.req);
         continue; // continue if there was an error and we're tolerant.
       }
       NamedList<NamedList<?>> secondPhaseResult =
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 0d3f746acb0..59f0eb4d8e3 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
@@ -41,6 +41,7 @@ import org.apache.solr.common.SolrException.ErrorCode;
 import org.apache.solr.common.params.ModifiableSolrParams;
 import org.apache.solr.common.params.ShardParams;
 import org.apache.solr.common.util.NamedList;
+import org.apache.solr.handler.component.HttpShardHandler;
 import org.apache.solr.handler.component.ResponseBuilder;
 import org.apache.solr.handler.component.ShardRequest;
 import org.apache.solr.handler.component.ShardResponse;
@@ -100,8 +101,7 @@ public class ExactStatsCache extends StatsCache {
   protected void doMergeToGlobalStats(SolrQueryRequest req, 
List<ShardResponse> responses) {
     Set<Term> allTerms = new HashSet<>();
     for (ShardResponse r : responses) {
-      if 
("true".equalsIgnoreCase(req.getParams().get(ShardParams.SHARDS_TOLERANT))
-          && r.getException() != null) {
+      if (HttpShardHandler.getShardsTolerantAsBool(req) && 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);
diff --git a/solr/core/src/java/org/apache/solr/util/SolrResponseUtil.java 
b/solr/core/src/java/org/apache/solr/util/SolrResponseUtil.java
index eb12dc9d779..660465552d0 100644
--- a/solr/core/src/java/org/apache/solr/util/SolrResponseUtil.java
+++ b/solr/core/src/java/org/apache/solr/util/SolrResponseUtil.java
@@ -20,8 +20,8 @@ import java.lang.invoke.MethodHandles;
 import java.util.Objects;
 import org.apache.solr.client.solrj.SolrResponse;
 import org.apache.solr.common.SolrException;
-import org.apache.solr.common.params.ShardParams;
 import org.apache.solr.common.util.NamedList;
+import org.apache.solr.handler.component.HttpShardHandler;
 import org.apache.solr.handler.component.ResponseBuilder;
 import org.apache.solr.handler.component.ShardResponse;
 import org.apache.solr.response.SolrQueryResponse;
@@ -64,7 +64,7 @@ public class SolrResponseUtil {
         }
       }
     } catch (Exception ex) {
-      if (rb != null && 
ShardParams.getShardsTolerantAsBool(rb.req.getParams())) {
+      if (rb != null && HttpShardHandler.getShardsTolerantAsBool(rb.req)) {
         return null;
       } else {
         throw new SolrException(
diff --git a/solr/core/src/test/org/apache/solr/search/TestCpuAllowedLimit.java 
b/solr/core/src/test/org/apache/solr/search/TestCpuAllowedLimit.java
index 2a61aa9eebd..274994099f1 100644
--- a/solr/core/src/test/org/apache/solr/search/TestCpuAllowedLimit.java
+++ b/solr/core/src/test/org/apache/solr/search/TestCpuAllowedLimit.java
@@ -166,7 +166,7 @@ public class TestCpuAllowedLimit extends SolrCloudTestCase {
                 "timeAllowed",
                 "500"));
     // System.err.println("rsp=" + rsp.jsonStr());
-    assertNotNull("should have partial results", 
rsp.getHeader().get("partialResults"));
+    assertEquals("should have partial results", true, 
rsp.getHeader().get("partialResults"));
 
     log.info("--- timeAllowed, partial results, multithreading ---");
     rsp =
@@ -222,6 +222,28 @@ public class TestCpuAllowedLimit extends SolrCloudTestCase 
{
                 "false"));
     // System.err.println("rsp=" + rsp.jsonStr());
     assertNotNull("should have partial results", 
rsp.getHeader().get("partialResults"));
+    log.info("--- cpuAllowed 1, partial results omitted ---");
+    rsp =
+        solrClient.query(
+            COLLECTION,
+            params(
+                "q",
+                "id:*",
+                "sort",
+                "id desc",
+                "stages",
+                "prepare,process",
+                "cpuAllowed",
+                "100",
+                "partialResults",
+                "false",
+                "multiThreaded",
+                "false",
+                "_",
+                "foo"));
+    String s = rsp.jsonStr();
+    System.err.println("rsp=" + s);
+    assertEquals("should have partial results", "omitted", 
rsp.getHeader().get("partialResults"));
 
     // cpuAllowed set, should return partial results
     log.info("--- cpuAllowed 2, partial results, multi-threaded ---");
diff --git 
a/solr/solr-ref-guide/modules/query-guide/pages/common-query-parameters.adoc 
b/solr/solr-ref-guide/modules/query-guide/pages/common-query-parameters.adoc
index cdfa1826235..bc8cd641690 100644
--- a/solr/solr-ref-guide/modules/query-guide/pages/common-query-parameters.adoc
+++ b/solr/solr-ref-guide/modules/query-guide/pages/common-query-parameters.adoc
@@ -324,9 +324,20 @@ The default value of this parameter is blank, which causes 
no extra "explain inf
 
 This parameter controls Solr's behavior when a query execution limit is 
reached (e.g. `timeAllowed` or `cpuAllowed`).
 
-When this parameter is set to `true` (default) then even though reaching a 
limit terminates further query processing  Solr will still attempt to return 
partial results collected so far. These results may be incomplete in a 
non-deterministic way (e.g. only some matching documents, documents without 
fields, missing facets or pivots, no spellcheck results, etc).
+When this parameter is set to `true` (default) then Solr will return the 
results collected prior to detecting the limit violation.
+If `shards.tolerant=true` is also set, all non-limited responses will still be 
collected. If you are seeking a best-effort response use of the 
xref:deployment-guide:solrcloud-distributed-requests.adoc#shards-tolerant-parameter[shards.tolerant
 Parameter] is recommended.
+
+If incomplete results are returned the `partialResults` response header will 
be set to `true`
+
+WARNING: These results may be incomplete in a non-deterministic way (e.g. only 
some matching documents, documents without fields, missing facets or pivots, no 
spellcheck results, etc).
+
+
+When this parameter is set to `false` then upon reaching a limit, Solr will 
stop collecting results and will not return any partial results already 
collected.
+In this case, the partialResults response header will be set to `omitted` and 
no result documents are returned.
+
+NOTE: The response will be 200 OK because the system has correctly provided 
the requested behavior.
+This is not an error condition.
 
-When this parameter is set to `false` then reaching a limit will generate an 
exception and any partial results collected so far will be discarded.
 
 == timeAllowed Parameter
 
diff --git 
a/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-9.adoc 
b/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-9.adoc
index e9890dfe308..f448fa7bc0e 100644
--- 
a/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-9.adoc
+++ 
b/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-9.adoc
@@ -71,6 +71,12 @@ Due to changes in Lucene 9, that isn't possible any more.
 === Configuration
 In solrconfig.xml, the `numVersionBuckets` and `versionBucketLockTimeoutMs` 
settings are now obsolete and ignored; a warning will be logged if specified.
 
+=== Partial Results
+When query limits are in use and partial results are not desirable (i.e. 
reporting or quantitative usages of search)
+users may pass `partialResults=false`.
+This feature has been improved to eliminate some wasted processing.
+In the presence of this parameter, queries encountering limits will return 
zero results, and to make it clear when this has happened, the `partialResults` 
response header will be set to `"omitted"`
+
 == Solr 9.7
 === SchemaVersion upgraded to 1.7
 The default schemaVersion is now 1.7.
diff --git a/solr/solrj/src/java/org/apache/solr/common/params/ShardParams.java 
b/solr/solrj/src/java/org/apache/solr/common/params/ShardParams.java
index 273db7651e8..a96a5d4943d 100644
--- a/solr/solrj/src/java/org/apache/solr/common/params/ShardParams.java
+++ b/solr/solrj/src/java/org/apache/solr/common/params/ShardParams.java
@@ -16,8 +16,6 @@
  */
 package org.apache.solr.common.params;
 
-import org.apache.solr.common.util.StrUtils;
-
 /**
  * Parameters used for distributed search.
  *
@@ -59,7 +57,7 @@ public interface ShardParams {
   /** Request detailed match info for each shard (true/false) */
   String SHARDS_INFO = "shards.info";
 
-  /** Should things fail if there is an error? (true/false/{@value 
#REQUIRE_ZK_CONNECTED}) */
+  /** Should things fail if there is an error? (true/false/requireZkConnected) 
*/
   String SHARDS_TOLERANT = "shards.tolerant";
 
   /** query purpose for shard requests */
@@ -104,26 +102,10 @@ public interface ShardParams {
   String DISTRIB_SINGLE_PASS = "distrib.singlePass";
 
   /**
-   * Throw an error from search requests when the {@value #SHARDS_TOLERANT} 
param has this value and
-   * ZooKeeper is not connected.
+   * Throw an error from search requests when the {@value 
ShardParams#SHARDS_TOLERANT} param has
+   * this value and ZooKeeper is not connected.
    *
-   * @see #getShardsTolerantAsBool(SolrParams)
+   * <p>See also HttpShardHandler's getShardsTolerantAsBool(SolrQueryRequest)
    */
   String REQUIRE_ZK_CONNECTED = "requireZkConnected";
-
-  /**
-   * Parse the {@value #SHARDS_TOLERANT} param from <code>params</code> as a 
boolean; accepts
-   * {@value #REQUIRE_ZK_CONNECTED} as a valid value indicating 
<code>false</code>.
-   *
-   * <p>By default, returns <code>false</code> when {@value #SHARDS_TOLERANT} 
is not set in <code>
-   * params</code>.
-   */
-  static boolean getShardsTolerantAsBool(SolrParams params) {
-    String shardsTolerantValue = params.get(SHARDS_TOLERANT);
-    if (null == shardsTolerantValue || 
shardsTolerantValue.equals(REQUIRE_ZK_CONNECTED)) {
-      return false;
-    } else {
-      return StrUtils.parseBool(shardsTolerantValue.trim()); // throw an 
exception if non-boolean
-    }
-  }
 }
diff --git 
a/solr/solrj/src/test/org/apache/solr/common/params/ShardParamsTest.java 
b/solr/solrj/src/test/org/apache/solr/common/params/ShardParamsTest.java
index 63f09d644d3..32ebca5c75e 100644
--- a/solr/solrj/src/test/org/apache/solr/common/params/ShardParamsTest.java
+++ b/solr/solrj/src/test/org/apache/solr/common/params/ShardParamsTest.java
@@ -18,6 +18,9 @@ package org.apache.solr.common.params;
 
 import org.apache.solr.SolrTestCase;
 import org.apache.solr.common.SolrException;
+import org.apache.solr.handler.component.HttpShardHandler;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.request.SolrQueryRequestBase;
 import org.junit.Test;
 
 /**
@@ -76,29 +79,31 @@ public class ShardParamsTest extends SolrTestCase {
   @Test
   public void testGetShardsTolerantAsBool() {
     ModifiableSolrParams params = new ModifiableSolrParams();
+    SolrQueryRequest paramsSupplier = new SolrQueryRequestBase(null, params) 
{};
     // shards.tolerant param is not set; default should be false
-    assertFalse(ShardParams.getShardsTolerantAsBool(params));
+    assertFalse(HttpShardHandler.getShardsTolerantAsBool(paramsSupplier));
 
     // shards.tolerant boolean true param should return true
     for (String trueValue : new String[] {"true", "yes", "on"}) {
       params.set(ShardParams.SHARDS_TOLERANT, trueValue);
-      assertTrue(ShardParams.getShardsTolerantAsBool(params));
+      assertTrue(HttpShardHandler.getShardsTolerantAsBool(paramsSupplier));
     }
 
     // shards.tolerant boolean false param should return false
     for (String falseValue : new String[] {"false", "no", "off"}) {
       params.set(ShardParams.SHARDS_TOLERANT, falseValue);
-      assertFalse(ShardParams.getShardsTolerantAsBool(params));
+      assertFalse(HttpShardHandler.getShardsTolerantAsBool(paramsSupplier));
     }
 
     // shards.tolerant=requireZkConnected should return false
     params.set(ShardParams.SHARDS_TOLERANT, ShardParams.REQUIRE_ZK_CONNECTED);
-    assertFalse(ShardParams.getShardsTolerantAsBool(params));
+    assertFalse(HttpShardHandler.getShardsTolerantAsBool(paramsSupplier));
 
     // values that aren't "requireZkConnected" or boolean should throw an 
exception
     params.set(ShardParams.SHARDS_TOLERANT, "bogusValue");
     Exception exception =
-        expectThrows(SolrException.class, () -> 
ShardParams.getShardsTolerantAsBool(params));
+        expectThrows(
+            SolrException.class, () -> 
HttpShardHandler.getShardsTolerantAsBool(paramsSupplier));
     assertTrue(
         exception.getMessage(), exception.getMessage().startsWith("invalid 
boolean value: "));
   }

Reply via email to