From a0dfe2add696452c7f27d76f2bbb9958854f4604 Mon Sep 17 00:00:00 2001 From: Mark Robert Miller Date: Fri, 15 May 2026 19:27:43 -0500 Subject: [PATCH] Switch HttpShardHandler shard fan-out to virtual threads Submit each shard request as a virtual-thread task that calls lbClient.requestAsync(lbReq).get() to do the request setup, dispatch, and waiting on a single virtual thread. The submitter thread races through the submit loop without paying the per-shard CPU cost (PKI signing, request building, sysprop reads, etc.); those happen in parallel across the virtual threads. Cancellation: cancelAll() interrupts the virtual thread via the executor Future stored in pending. The InterruptedException catch inside the task calls jettyFuture.cancel(true) which is the graceful abort path Jetty's async machinery is designed for, instead of relying on a thread-interrupt of a synchronous send (which races the connection cleanup under stress). ParallelHttpShardHandlerFactory and ParallelHttpShardHandler are removed; the default HttpShardHandlerFactory now provides the parallel-submit / parallel-completion behavior natively, and is the only built-in implementation. Because shardExecutor is a plain Executors.newThreadPerTaskExecutor rather than MDCAwareThreadPoolExecutor, each shard task explicitly propagates the submitter's MDC, marks the virtual thread as a Solr server thread, and replays every registered ExecutorUtil.InheritableThreadLocalProvider on entry. This carries SolrRequestInfo (used by PKI to sign as the calling user), the OpenTelemetry trace Context, and any other registered provider onto the virtual thread, matching the contract MDCAwareThreadPoolExecutor follows for pool threads. ExecutorUtil now exposes getThreadLocalProviders() to allow this iteration from outside the class. The requestAsync call is wrapped in AccessController.doPrivileged so per-request "solr.*" sysprop reads succeed under SecurityManager when the call originates from a virtual thread. --- .../http-shard-handler-virtual-threads.yml | 6 + .../handler/component/HttpShardHandler.java | 154 +++++++++++++----- .../component/HttpShardHandlerFactory.java | 33 +++- .../component/ParallelHttpShardHandler.java | 126 -------------- .../ParallelHttpShardHandlerFactory.java | 26 --- .../ParallelHttpShardHandlerTest.java | 108 ------------ .../component/TestShardHandlerFactory.java | 6 +- .../pages/configuring-solr-xml.adoc | 7 +- .../apache/solr/common/util/ExecutorUtil.java | 5 + 9 files changed, 161 insertions(+), 310 deletions(-) create mode 100644 changelog/unreleased/http-shard-handler-virtual-threads.yml delete mode 100644 solr/core/src/java/org/apache/solr/handler/component/ParallelHttpShardHandler.java delete mode 100644 solr/core/src/java/org/apache/solr/handler/component/ParallelHttpShardHandlerFactory.java delete mode 100644 solr/core/src/test/org/apache/solr/handler/component/ParallelHttpShardHandlerTest.java diff --git a/changelog/unreleased/http-shard-handler-virtual-threads.yml b/changelog/unreleased/http-shard-handler-virtual-threads.yml new file mode 100644 index 000000000000..1bd67c625341 --- /dev/null +++ b/changelog/unreleased/http-shard-handler-virtual-threads.yml @@ -0,0 +1,6 @@ +# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc +title: HttpShardHandler now uses Java virtual threads for shard fan-out, replacing CompletableFuture callbacks; the now-equivalent ParallelHttpShardHandlerFactory is removed (switch to the default HttpShardHandlerFactory). +type: changed +authors: + - name: Mark Robert Miller +links: [] 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 431652720277..2c28b438c07f 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 @@ -20,16 +20,26 @@ import static org.apache.solr.request.SolrQueryRequest.disallowPartialResults; import java.lang.invoke.MethodHandles; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import net.jcip.annotations.NotThreadSafe; import org.apache.solr.client.solrj.SolrRequest; import org.apache.solr.client.solrj.SolrResponse; @@ -47,6 +57,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.ExecutorUtil; import org.apache.solr.common.util.NamedList; import org.apache.solr.common.util.StrUtils; import org.apache.solr.core.CoreDescriptor; @@ -55,6 +66,7 @@ import org.apache.solr.security.AllowListUrlChecker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.slf4j.MDC; /** * Solr's default {@link ShardHandler} implementation; uses Jetty's async HTTP Client APIs for @@ -91,19 +103,20 @@ public class HttpShardHandler extends ShardHandler { private final HttpShardHandlerFactory httpShardHandlerFactory; - protected final ConcurrentMap> - responseFutureMap; + protected final ConcurrentMap> pending; protected final BlockingQueue responses; private final AtomicBoolean canceled = new AtomicBoolean(false); private final Map> shardToURLs; protected LBAsyncSolrClient lbClient; + private final ExecutorService shardExecutor; public HttpShardHandler(HttpShardHandlerFactory httpShardHandlerFactory) { this.httpShardHandlerFactory = httpShardHandlerFactory; this.lbClient = httpShardHandlerFactory.loadbalancer; + this.shardExecutor = httpShardHandlerFactory.shardExecutor; this.responses = new LinkedBlockingQueue<>(); - this.responseFutureMap = new ConcurrentHashMap<>(); + this.pending = new ConcurrentHashMap<>(); // maps "localhost:8983|localhost:7574" to a shuffled // List("http://localhost:8983","http://localhost:7574") @@ -265,40 +278,108 @@ protected void makeShardRequest( SimpleSolrResponse ssr, ShardResponse srsp, long startTimeNS) { - CompletableFuture future = this.lbClient.requestAsync(lbReq); - // Synchronize on canceled, so that we know precisely whether to add it to the responseFutureMap - // or not. - synchronized (canceled) { - if (canceled.get() && !future.isDone()) { - future.cancel(true); - return; - } else { - responseFutureMap.put(srsp, future); + // Capture submitter context now so the shard task (running on a virtual thread) sees the + // submitter's MDC + every registered InheritableThreadLocalProvider (SolrRequestInfo for PKI, + // OTel trace Context, etc.), matching what MDCAwareThreadPoolExecutor does for pool threads. + // Virtual threads aren't pooled so no restore is needed. + final Map submitterMdc = MDC.getCopyOfContextMap(); + final List providers = + ExecutorUtil.getThreadLocalProviders(); + final List> providerCtx; + if (providers.isEmpty()) { + providerCtx = List.of(); + } else { + providerCtx = new ArrayList<>(providers.size()); + for (ExecutorUtil.InheritableThreadLocalProvider p : providers) { + AtomicReference ref = new AtomicReference<>(); + p.store(ref); + providerCtx.add(ref); } } - // Add the callback explicitly after adding the future to the map, because the callback relies - // on the map already having the future. - future.whenComplete( - (LBSolrClient.Rsp rsp, Throwable throwable) -> { - if (rsp != null) { - ssr.nl = rsp.getResponse(); - srsp.setShardAddress(rsp.getServer()); - } else if (throwable != null) { - srsp.setException(throwable); - if (throwable instanceof SolrException) { - srsp.setResponseCode(((SolrException) throwable).code()); - } + + // Build + dispatch the request on the virtual thread (parallel CPU work) and block on the + // returned CompletableFuture there. cancelAll() interrupts the virtual thread; the catch + // below translates that into a graceful jetty-future cancel, which is how Jetty wants to be + // aborted (vs. interrupting a synchronous send mid-flight). + Runnable shardTask = + () -> { + ExecutorUtil.setServerThreadFlag(Boolean.TRUE); + if (submitterMdc != null) { + MDC.setContextMap(submitterMdc); + } + for (int i = 0; i < providers.size(); i++) { + providers.get(i).set(providerCtx.get(i)); } - ssr.elapsedTime = - TimeUnit.MILLISECONDS.convert(System.nanoTime() - startTimeNS, TimeUnit.NANOSECONDS); - // Synchronize on cancelled so this code and cancelAll() cannot happen at the same time - synchronized (canceled) { - // We don't want to add responses after the requests have been canceled - if (responseFutureMap.containsKey(srsp)) { - responses.add(HttpShardHandler.this.transformResponse(sreq, srsp, shard)); + CompletableFuture jettyFuture = null; + try { + try { + // doPrivileged needed because the request setup reads "solr.*" system properties + // and otherwise fails under SecurityManager when invoked from a virtual thread. + @SuppressWarnings("removal") + CompletableFuture tmp = + AccessController.doPrivileged( + (PrivilegedExceptionAction>) + () -> lbClient.requestAsync(lbReq)); + jettyFuture = tmp; + LBSolrClient.Rsp rsp = jettyFuture.get(); + ssr.nl = rsp.getResponse(); + srsp.setShardAddress(rsp.getServer()); + } catch (CancellationException ce) { + // jettyFuture was cancelled; leave srsp without an exception/response. + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + if (jettyFuture != null) { + jettyFuture.cancel(true); + } + } catch (ExecutionException ee) { + Throwable cause = ee.getCause() != null ? ee.getCause() : ee; + srsp.setException(cause); + if (cause instanceof SolrException se) { + srsp.setResponseCode(se.code()); + } + } catch (PrivilegedActionException pae) { + Throwable cause = pae.getCause() != null ? pae.getCause() : pae; + srsp.setException(cause); + if (cause instanceof SolrException se) { + srsp.setResponseCode(se.code()); + } + } + } finally { + ssr.elapsedTime = + TimeUnit.MILLISECONDS.convert( + System.nanoTime() - startTimeNS, TimeUnit.NANOSECONDS); + // Synchronize on canceled so this code and cancelAll() cannot happen at the same time + synchronized (canceled) { + // We don't want to add responses after the requests have been canceled + if (pending.containsKey(srsp)) { + responses.add(HttpShardHandler.this.transformResponse(sreq, srsp, shard)); + } } + for (int i = 0; i < providers.size(); i++) { + providers.get(i).clean(providerCtx.get(i)); + } + MDC.clear(); } - }); + }; + + // Submit cheaply; the coordinator thread races through the loop while CPU-heavy request + // setup happens in parallel on the virtual threads. + synchronized (canceled) { + if (canceled.get()) { + return; + } + try { + Future vtFuture = shardExecutor.submit(shardTask); + pending.put(srsp, vtFuture); + } catch (RejectedExecutionException ree) { + recordShardSubmitError( + srsp, + new SolrException( + SolrException.ErrorCode.SERVER_ERROR, + "Shard executor rejected request for shard: " + shard, + ree)); + } + } } /** Subclasses could modify the request based on the shard */ @@ -349,7 +430,7 @@ private ShardResponse take(boolean bailOnError) { .orElse(previousResponse); } } else { - responseFutureMap.remove(rsp); + pending.remove(rsp); // add response to the response list... we do this after the take() and // not after the completion of "call" so we know when the last response @@ -375,7 +456,7 @@ private ShardResponse take(boolean bailOnError) { } protected boolean responsesPending() { - return !responseFutureMap.isEmpty() || !responses.isEmpty(); + return !pending.isEmpty() || !responses.isEmpty(); } @Override @@ -394,13 +475,12 @@ public void cancelAll() { // We don't want to queue this multiple times if we are already canceled responses.add(CANCELLATION_NOTIFICATION); } - // Cancel all outstanding requests - for (CompletableFuture future : responseFutureMap.values()) { + for (Future future : pending.values()) { if (!future.isDone()) { future.cancel(true); } } - responseFutureMap.clear(); + pending.clear(); } } diff --git a/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandlerFactory.java b/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandlerFactory.java index 574a0b266a37..ceaf9db0b85f 100644 --- a/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandlerFactory.java +++ b/solr/core/src/java/org/apache/solr/handler/component/HttpShardHandlerFactory.java @@ -28,6 +28,7 @@ import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -84,6 +85,13 @@ public class HttpShardHandlerFactory extends ShardHandlerFactory // This executor is initialized in the init method protected ExecutorService commExecutor; + /** + * Virtual-thread-per-task executor used for the per-shard scatter/gather fan-out in {@link + * HttpShardHandler}. Distinct from {@link #commExecutor}, which serves as Jetty's HTTP client + * worker executor. + */ + protected ExecutorService shardExecutor; + protected volatile HttpJettySolrClient defaultClient; protected InstrumentedHttpListenerFactory httpListenerFactory; protected LBAsyncSolrClient loadbalancer; @@ -286,6 +294,10 @@ public void init(PluginInfo info) { // collection as an optimization. see SOLR-11880 for more details false); + this.shardExecutor = + Executors.newThreadPerTaskExecutor( + Thread.ofVirtual().name("httpShardVT-", 0).factory()); + this.httpListenerFactory = new InstrumentedHttpListenerFactory(this.metricNameStrategy); int connectionTimeout = getParameter( @@ -341,16 +353,24 @@ protected T getParameter( @Override public void close() { try { - if (loadbalancer != null) { - loadbalancer.close(); + // Stop the per-shard virtual-thread executor first so in-flight tasks are interrupted before + // the load balancer / HTTP client they depend on are torn down. + if (shardExecutor != null) { + ExecutorUtil.shutdownNowAndAwaitTermination(shardExecutor); } } finally { try { - if (defaultClient != null) { - IOUtils.closeQuietly(defaultClient); + if (loadbalancer != null) { + loadbalancer.close(); } } finally { - ExecutorUtil.shutdownAndAwaitTermination(commExecutor); + try { + if (defaultClient != null) { + IOUtils.closeQuietly(defaultClient); + } + } finally { + ExecutorUtil.shutdownAndAwaitTermination(commExecutor); + } } } IOUtils.closeQuietly(asyncRequestsGauge); @@ -459,6 +479,9 @@ public void initializeMetrics(SolrMetricsContext parentContext, Attributes attri commExecutor = solrMetricsContext.instrumentedExecutorService( commExecutor, "solr.core.executor", "httpShardExecutor", SolrInfoBean.Category.QUERY); + shardExecutor = + solrMetricsContext.instrumentedExecutorService( + shardExecutor, "solr.core.executor", "httpShardVT", SolrInfoBean.Category.QUERY); if (defaultClient != null) { asyncRequestsGauge = solrMetricsContext.observableLongGauge( 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 deleted file mode 100644 index 5eb9c992ed93..000000000000 --- a/solr/core/src/java/org/apache/solr/handler/component/ParallelHttpShardHandler.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * 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.lang.invoke.MethodHandles; -import java.util.concurrent.CancellationException; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.ExecutorService; -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; - -/** - * A version of {@link HttpShardHandler} optimized for massively-sharded collections. - * - *

Uses a {@link HttpShardHandlerFactory#commExecutor} thread for all work related to outgoing - * requests, allowing {@link #submit(ShardRequest, String, ModifiableSolrParams)} to return more - * quickly. (See {@link HttpShardHandler} for comparison.) - * - *

The additional focus on parallelization makes this an ideal implementation for collections - * with many shards. - */ -@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 - * 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. - */ - private final ConcurrentMap> submitFutures; - - public ParallelHttpShardHandler(ParallelHttpShardHandlerFactory httpShardHandlerFactory) { - super(httpShardHandlerFactory); - this.commExecutor = httpShardHandlerFactory.commExecutor; - this.submitFutures = new ConcurrentHashMap<>(); - } - - @Override - protected boolean responsesPending() { - // ensure we can't exit while loop in HttpShardHandler.take(boolean) until we've completed - // submitting all of the shard requests - return super.responsesPending() || !submitFutures.isEmpty(); - } - - @Override - protected void makeShardRequest( - ShardRequest sreq, - String shard, - ModifiableSolrParams params, - LBSolrClient.Req lbReq, - SimpleSolrResponse ssr, - ShardResponse srsp, - long startTimeNS) { - CompletableFuture completableFuture = - CompletableFuture.runAsync( - () -> super.makeShardRequest(sreq, shard, params, lbReq, ssr, srsp, startTimeNS), - commExecutor); - submitFutures.put(srsp, completableFuture); - completableFuture.whenComplete( - (r, t) -> { - try { - if (t != null) { - Throwable failure = t; - if (failure instanceof CompletionException completionException - && completionException.getCause() != null) { - failure = completionException.getCause(); - } - if (!(failure instanceof CancellationException)) { - recordShardSubmitError( - srsp, - new SolrException( - SolrException.ErrorCode.SERVER_ERROR, - "Exception occurred while trying to send a request to shard: " + shard, - failure)); - } - } - } finally { - // Remove so that we keep track of in-flight submits only - submitFutures.remove(srsp); - } - }); - } - - @Override - public void cancelAll() { - super.cancelAll(); - submitFutures - .values() - .forEach( - future -> { - if (!future.isDone()) { - future.cancel(true); - } - }); - submitFutures.clear(); - } -} diff --git a/solr/core/src/java/org/apache/solr/handler/component/ParallelHttpShardHandlerFactory.java b/solr/core/src/java/org/apache/solr/handler/component/ParallelHttpShardHandlerFactory.java deleted file mode 100644 index 38b2cb9a974f..000000000000 --- a/solr/core/src/java/org/apache/solr/handler/component/ParallelHttpShardHandlerFactory.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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; - -/** Creates {@link ParallelHttpShardHandler} instances */ -public class ParallelHttpShardHandlerFactory extends HttpShardHandlerFactory { - - @Override - public ShardHandler getShardHandler() { - return new ParallelHttpShardHandler(this); - } -} diff --git a/solr/core/src/test/org/apache/solr/handler/component/ParallelHttpShardHandlerTest.java b/solr/core/src/test/org/apache/solr/handler/component/ParallelHttpShardHandlerTest.java deleted file mode 100644 index 4eb0db8858ff..000000000000 --- a/solr/core/src/test/org/apache/solr/handler/component/ParallelHttpShardHandlerTest.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * 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.util.List; -import java.util.concurrent.AbstractExecutorService; -import java.util.concurrent.TimeUnit; -import org.apache.solr.SolrTestCaseJ4; -import org.apache.solr.client.solrj.impl.LBSolrClient; -import org.apache.solr.client.solrj.request.QueryRequest; -import org.apache.solr.common.SolrException; -import org.apache.solr.common.params.ModifiableSolrParams; -import org.junit.Test; - -public class ParallelHttpShardHandlerTest extends SolrTestCaseJ4 { - - private static class DirectExecutorService extends AbstractExecutorService { - private volatile boolean shutdown; - - @Override - public void shutdown() { - shutdown = true; - } - - @Override - public List shutdownNow() { - shutdown = true; - return List.of(); - } - - @Override - public boolean isShutdown() { - return shutdown; - } - - @Override - public boolean isTerminated() { - return shutdown; - } - - @Override - public boolean awaitTermination(long timeout, TimeUnit unit) { - return shutdown; - } - - @Override - public void execute(Runnable command) { - command.run(); - } - } - - @Test - public void testSubmitFailureIsRecordedWhenSuperThrows() throws Exception { - ParallelHttpShardHandlerFactory factory = new ParallelHttpShardHandlerFactory(); - factory.commExecutor = new DirectExecutorService(); - ParallelHttpShardHandler handler = new ParallelHttpShardHandler(factory); - - // Force super.makeShardRequest to throw before it enqueues the response future. - handler.lbClient = null; - - ShardRequest shardRequest = new ShardRequest(); - shardRequest.params = new ModifiableSolrParams(); - shardRequest.actualShards = new String[] {"shardA"}; - - ShardResponse shardResponse = new ShardResponse(); - shardResponse.setShardRequest(shardRequest); - shardResponse.setShard("shardA"); - - HttpShardHandler.SimpleSolrResponse simpleResponse = new HttpShardHandler.SimpleSolrResponse(); - shardResponse.setSolrResponse(simpleResponse); - - ModifiableSolrParams params = new ModifiableSolrParams(); - QueryRequest queryRequest = new QueryRequest(params); - LBSolrClient.Endpoint endpoint = new LBSolrClient.Endpoint("http://ignored:8983/solr"); - LBSolrClient.Req lbReq = new LBSolrClient.Req(queryRequest, List.of(endpoint)); - - handler.makeShardRequest( - shardRequest, "shardA", params, lbReq, simpleResponse, shardResponse, System.nanoTime()); - - ShardResponse recorded = handler.responses.poll(1, TimeUnit.SECONDS); - - assertNotNull( - "The asynchronous submit should record the shard failure when super.makeShardRequest fails", - recorded); - assertSame( - "The recorded shard response should be the same instance passed into recordShardSubmitError", - shardResponse, - recorded); - assertNotNull( - "Expected an exception to be attached to the recorded shard response", - recorded.getException()); - assertTrue(recorded.getException() instanceof SolrException); - } -} diff --git a/solr/core/src/test/org/apache/solr/handler/component/TestShardHandlerFactory.java b/solr/core/src/test/org/apache/solr/handler/component/TestShardHandlerFactory.java index 59325e9b55e9..f7a342230672 100644 --- a/solr/core/src/test/org/apache/solr/handler/component/TestShardHandlerFactory.java +++ b/solr/core/src/test/org/apache/solr/handler/component/TestShardHandlerFactory.java @@ -36,13 +36,11 @@ import org.junit.BeforeClass; import org.junit.Test; -/** Tests exercising Solr's two "out-of-the-box" ShardHandlerFactory implementations */ +/** Tests exercising Solr's "out-of-the-box" {@link HttpShardHandlerFactory} */ public class TestShardHandlerFactory extends SolrTestCaseJ4 { private static final String[] SHARD_HANDLER_FACTORY_IMPLEMENTATIONS = - new String[] { - HttpShardHandlerFactory.class.getName(), ParallelHttpShardHandlerFactory.class.getName() - }; + new String[] {HttpShardHandlerFactory.class.getName()}; private static final String LOAD_BALANCER_REQUESTS_MIN_ABSOLUTE = "solr.tests.loadBalancerRequestsMinimumAbsolute"; diff --git a/solr/solr-ref-guide/modules/configuration-guide/pages/configuring-solr-xml.adoc b/solr/solr-ref-guide/modules/configuration-guide/pages/configuring-solr-xml.adoc index 66e6d21dc7ac..64e478c5fab6 100644 --- a/solr/solr-ref-guide/modules/configuration-guide/pages/configuring-solr-xml.adoc +++ b/solr/solr-ref-guide/modules/configuration-guide/pages/configuring-solr-xml.adoc @@ -509,9 +509,8 @@ Solr uses "Shard Handlers" to send and track the inter-node requests made intern A factory, configured via the `` element, is used to create new Shard Handlers as needed. The factory defined here will be used throughout Solr, unless overridden by particular requestHandler's in solrconfig.xml. -Two factory implementations are available, each creating a corresponding Shard Handler. -The default, `HttpShardHandlerFactory`, serves as the best option for most deployments. -However some deployments, especially those using authentication or with massively sharded collections, may benefit from the additional parallelization offered by `ParallelHttpShardHandlerFactory`. +Solr ships with a single built-in factory implementation, `HttpShardHandlerFactory`, which dispatches each shard request on its own virtual thread. +This is the default and serves the needs of all deployments, including those with massively sharded collections. Custom shard handlers are also supported and should be referenced in `solr.xml` by their fully-qualified class name: @@ -520,7 +519,7 @@ Custom shard handlers are also supported and should be referenced in `solr.xml` ---- -Sub-elements of `` may vary in the case of custom shard handlers, but both `HttpShardHandlerFactory` and `ParallelShardHandlerFactory` support the following configuration options: +Sub-elements of `` may vary in the case of custom shard handlers, but `HttpShardHandlerFactory` supports the following configuration options: [[sockettimeout]]`socketTimeout`:: + diff --git a/solr/solrj/src/java/org/apache/solr/common/util/ExecutorUtil.java b/solr/solrj/src/java/org/apache/solr/common/util/ExecutorUtil.java index 48ac61f55d87..0ca441671561 100644 --- a/solr/solrj/src/java/org/apache/solr/common/util/ExecutorUtil.java +++ b/solr/solrj/src/java/org/apache/solr/common/util/ExecutorUtil.java @@ -65,6 +65,11 @@ public static synchronized void addThreadLocalProvider(InheritableThreadLocalPro providers = copy; } + /** Snapshot of currently-registered {@link InheritableThreadLocalProvider}s. */ + public static List getThreadLocalProviders() { + return providers; + } + /** * Any class which wants to carry forward the thread local values to the threads run by thread * pools must implement this interface and the implementation should be registered here