From 9131f94503f8849680235be20a00001598063453 Mon Sep 17 00:00:00 2001 From: Renato Haeberli Date: Thu, 23 Jul 2026 16:15:55 +0200 Subject: [PATCH 1/9] SOLR-18312: introduce dedicated thread pool executor for httpClientBuilder --- .../client/solrj/impl/HttpJdkSolrClient.java | 41 +++++++++++++---- .../solrj/impl/HttpJdkSolrClientTest.java | 46 +++++++++++++++++++ 2 files changed, 78 insertions(+), 9 deletions(-) diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java index 111bfd1bc92..5d53f0b5420 100644 --- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java @@ -42,6 +42,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.SynchronousQueue; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -78,28 +79,36 @@ public class HttpJdkSolrClient extends HttpSolrClient { protected HttpClient httpClient; + /** + * Executor used to stream (produce) request bodies into the pipe consumed by the JDK HttpClient. + * This is the "producer" side and may be supplied by the caller. + */ protected ExecutorService executor; + /** Dedicated executor handed to the JDK HttpClient */ + protected ExecutorService httpClientExecutor; + private boolean forceHttp11; private final boolean shutdownExecutor; protected HttpJdkSolrClient(String serverBaseUrl, HttpJdkSolrClient.Builder builder) { super(serverBaseUrl, builder); - HttpClient.Builder b = HttpClient.newBuilder(); + HttpClient.Builder httpClientBuilder = HttpClient.newBuilder(); HttpClient.Redirect followRedirects = Boolean.TRUE.equals(builder.getFollowRedirects()) ? HttpClient.Redirect.NORMAL : HttpClient.Redirect.NEVER; - b.followRedirects(followRedirects); + httpClientBuilder.followRedirects(followRedirects); - b.connectTimeout(Duration.of(builder.getConnectionTimeoutMillis(), ChronoUnit.MILLIS)); + httpClientBuilder.connectTimeout( + Duration.of(builder.getConnectionTimeoutMillis(), ChronoUnit.MILLIS)); // note: idle timeout isn't used for the JDK client // note: request timeout is set per request if (builder.sslContext != null) { - b.sslContext(builder.sslContext); + httpClientBuilder.sslContext(builder.sslContext); } if (builder.getExecutor() != null) { @@ -117,15 +126,23 @@ protected HttpJdkSolrClient(String serverBaseUrl, HttpJdkSolrClient.Builder buil new SolrNamedThreadFactory(this.getClass().getSimpleName())); this.shutdownExecutor = true; } - b.executor(this.executor); + this.httpClientExecutor = + new ExecutorUtil.MDCAwareThreadPoolExecutor( + 0, + Integer.MAX_VALUE, + 60, + TimeUnit.SECONDS, + new SynchronousQueue<>(), + new SolrNamedThreadFactory(this.getClass().getSimpleName() + "-http")); + httpClientBuilder.executor(this.httpClientExecutor); if (builder.shouldUseHttp1_1()) { this.forceHttp11 = true; - b.version(HttpClient.Version.HTTP_1_1); + httpClientBuilder.version(HttpClient.Version.HTTP_1_1); } if (builder.cookieHandler != null) { - b.cookieHandler(builder.cookieHandler); + httpClientBuilder.cookieHandler(builder.cookieHandler); } if (builder.getProxyHost() != null) { @@ -133,10 +150,10 @@ protected HttpJdkSolrClient(String serverBaseUrl, HttpJdkSolrClient.Builder buil log.warn( "Socks4 is likely not supported by this client. See https://bugs.openjdk.org/browse/JDK-8214516"); } - b.proxy( + httpClientBuilder.proxy( ProxySelector.of(new InetSocketAddress(builder.getProxyHost(), builder.getProxyPort()))); } - this.httpClient = b.build(); + this.httpClient = httpClientBuilder.build(); assert ObjectReleaseTracker.track(this); } @@ -545,6 +562,12 @@ public void close() throws IOException { } executor = null; + // The http client executor is always created and owned by this instance. + if (httpClientExecutor != null) { + ExecutorUtil.shutdownAndAwaitTermination(httpClientExecutor); + httpClientExecutor = null; + } + assert ObjectReleaseTracker.release(this); } diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/HttpJdkSolrClientTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/HttpJdkSolrClientTest.java index 79bfdb63e9c..9aef536d370 100644 --- a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/HttpJdkSolrClientTest.java +++ b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/HttpJdkSolrClientTest.java @@ -48,6 +48,7 @@ import org.apache.solr.client.solrj.request.SolrQuery; import org.apache.solr.client.solrj.request.UpdateRequest; import org.apache.solr.client.solrj.request.XMLRequestWriter; +import org.apache.solr.client.solrj.request.json.JsonQueryRequest; import org.apache.solr.client.solrj.response.JavaBinResponseParser; import org.apache.solr.client.solrj.response.ResponseParser; import org.apache.solr.client.solrj.response.SolrPingResponse; @@ -637,6 +638,51 @@ public void testMaybeTryHeadRequestHasContentType() throws Exception { } } + @Test(timeout = 30000) + public void testConcurrentStreamedBodiesDoNotDeadlockWithHttp1() throws Exception { + DebugServlet.clear(); + String url = solrTestRule.getBaseUrl() + DEBUG_SERVLET_PATH; + + int concurrency = 8; + ExecutorService callers = + ExecutorUtil.newMDCAwareFixedThreadPool(concurrency, new NamedThreadFactory("test-caller")); + + try (HttpJdkSolrClient client = builder(url).useHttp1_1(true).build()) { + List> futures = new ArrayList<>(concurrency); + for (int i = 0; i < concurrency; i++) { + futures.add( + CompletableFuture.runAsync( + () -> { + JsonQueryRequest q = buildLargeBodyQuery(); + try { + q.process(client); + } catch (Exception ignored) { + } + }, + callers)); + } + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .get(45, TimeUnit.SECONDS); + } finally { + ExecutorUtil.shutdownAndAwaitTermination(callers); + } + } + + private static JsonQueryRequest buildLargeBodyQuery() { + StringBuilder filter = new StringBuilder("id:("); + for (int i = 0; i < 400; i++) { + if (i > 0) { + filter.append(" OR "); + } + filter.append("value_").append(i); + } + filter.append(')'); + JsonQueryRequest q = new JsonQueryRequest(); + q.setQuery("*:*"); + q.withFilter(filter.toString()); + return q; + } + /** * This is not required for any test, but there appears to be a bug in the JDK client where it * does not release all threads if the client has not performed any queries, even after a forced From e79a86d9319c8e3f70c9389b28ca009f5f51e2a1 Mon Sep 17 00:00:00 2001 From: Renato Haeberli Date: Sat, 25 Jul 2026 12:22:18 +0200 Subject: [PATCH 2/9] SOLR-18312: improving test not to swallow exceptions --- .../solr/client/solrj/impl/HttpJdkSolrClientTest.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/HttpJdkSolrClientTest.java b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/HttpJdkSolrClientTest.java index 9aef536d370..9f9233f375e 100644 --- a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/HttpJdkSolrClientTest.java +++ b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/HttpJdkSolrClientTest.java @@ -641,6 +641,8 @@ public void testMaybeTryHeadRequestHasContentType() throws Exception { @Test(timeout = 30000) public void testConcurrentStreamedBodiesDoNotDeadlockWithHttp1() throws Exception { DebugServlet.clear(); + DebugServlet.addResponseHeader("Content-Type", "application/octet-stream"); + DebugServlet.responseBodyByQueryFragment.put("", javabinResponse()); String url = solrTestRule.getBaseUrl() + DEBUG_SERVLET_PATH; int concurrency = 8; @@ -656,13 +658,14 @@ public void testConcurrentStreamedBodiesDoNotDeadlockWithHttp1() throws Exceptio JsonQueryRequest q = buildLargeBodyQuery(); try { q.process(client); - } catch (Exception ignored) { + } catch (SolrServerException | IOException e) { + throw new RuntimeException(e); } }, callers)); } CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) - .get(45, TimeUnit.SECONDS); + .get(30, TimeUnit.SECONDS); } finally { ExecutorUtil.shutdownAndAwaitTermination(callers); } From 7e3956bdafb3549f62ec0cd20d70284c351361e5 Mon Sep 17 00:00:00 2001 From: David Smiley Date: Sat, 25 Jul 2026 23:21:51 -0400 Subject: [PATCH 3/9] Use newMDCAwareCachedThreadPool --- .../client/solrj/impl/HttpJdkSolrClient.java | 21 +++---------------- 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java index 5d53f0b5420..61964ca7eff 100644 --- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java @@ -37,13 +37,9 @@ import java.util.HashMap; import java.util.Locale; import java.util.Map; -import java.util.concurrent.BlockingQueue; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.SynchronousQueue; -import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.net.ssl.SSLContext; @@ -115,24 +111,13 @@ protected HttpJdkSolrClient(String serverBaseUrl, HttpJdkSolrClient.Builder buil this.executor = builder.getExecutor(); this.shutdownExecutor = false; } else { - BlockingQueue queue = new LinkedBlockingQueue<>(1024); this.executor = - new ExecutorUtil.MDCAwareThreadPoolExecutor( - 4, - 256, - 60, - TimeUnit.SECONDS, - queue, - new SolrNamedThreadFactory(this.getClass().getSimpleName())); + ExecutorUtil.newMDCAwareCachedThreadPool( + new SolrNamedThreadFactory(this.getClass().getSimpleName() + "-reqBody")); this.shutdownExecutor = true; } this.httpClientExecutor = - new ExecutorUtil.MDCAwareThreadPoolExecutor( - 0, - Integer.MAX_VALUE, - 60, - TimeUnit.SECONDS, - new SynchronousQueue<>(), + ExecutorUtil.newMDCAwareCachedThreadPool( new SolrNamedThreadFactory(this.getClass().getSimpleName() + "-http")); httpClientBuilder.executor(this.httpClientExecutor); From e534b380e080ee38d8af75d32f07884df6e1d02d Mon Sep 17 00:00:00 2001 From: David Smiley Date: Thu, 30 Jul 2026 01:18:06 -0400 Subject: [PATCH 4/9] Defer body thread usage until actually requested. --- .../client/solrj/impl/HttpJdkSolrClient.java | 103 ++++++++++-------- 1 file changed, 55 insertions(+), 48 deletions(-) diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java index 61964ca7eff..db810f23b8c 100644 --- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java @@ -21,6 +21,7 @@ import java.io.InputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; +import java.io.UncheckedIOException; import java.lang.invoke.MethodHandles; import java.net.CookieHandler; import java.net.InetSocketAddress; @@ -149,7 +150,7 @@ protected CompletableFuture> requestInputStreamAsync( PreparedRequest pReq = prepareRequest(baseUrl, solrRequest, collection); return httpClient .sendAsync(pReq.reqb.build(), HttpResponse.BodyHandlers.ofInputStream()) - .whenComplete((httpResponse, throwable) -> releaseContentWriting(pReq)); + .whenComplete((httpResponse, throwable) -> pReq.releaseContentWriting()); } catch (Exception e) { CompletableFuture> cf = new CompletableFuture<>(); cf.completeExceptionally(e); @@ -164,7 +165,7 @@ public CompletableFuture> requestAsync( PreparedRequest pReq = prepareRequest(null, solrRequest, collection); return httpClient .sendAsync(pReq.reqb.build(), HttpResponse.BodyHandlers.ofInputStream()) - .whenComplete((httpResponse, throwable) -> releaseContentWriting(pReq)) + .whenComplete((httpResponse, throwable) -> pReq.releaseContentWriting()) .thenApply( httpResponse -> { try { @@ -181,21 +182,6 @@ public CompletableFuture> requestAsync( } } - private void releaseContentWriting(PreparedRequest pReq) { - if (pReq.contentWritingFuture != null) { - pReq.contentWritingFuture.cancel(true); - } - // Closing the sink is what unblocks a writer already stuck in the pipe; cancel() alone does - // not. - if (pReq.contentWritingSink != null) { - try { - pReq.contentWritingSink.close(); - } catch (IOException e) { - log.warn("Could not close content-writing pipe", e); - } - } - } - @Override public NamedList requestWithBaseUrl( String baseUrl, SolrRequest solrRequest, String collection) @@ -216,9 +202,7 @@ public NamedList requestWithBaseUrl( } catch (RuntimeException e) { throw new SolrServerException(e); } finally { - if (pReq.contentWritingFuture != null) { - pReq.contentWritingFuture.cancel(true); - } + pReq.releaseContentWriting(); // See // https://docs.oracle.com/en/java/javase/17/docs/api/java.net.http/java/net/http/HttpResponse.BodySubscribers.html#ofInputStream() @@ -254,7 +238,7 @@ protected PreparedRequest prepareRequest( ResponseParser parserToUse = responseParser(solrRequest); ModifiableSolrParams queryParams = initializeSolrParams(solrRequest, parserToUse); var reqb = HttpRequest.newBuilder(); - PreparedRequest pReq = null; + PreparedRequest pReq; try { switch (solrRequest.getMethod()) { case GET: @@ -291,7 +275,7 @@ private PreparedRequest prepareGet( reqb.GET(); decorateRequest(reqb, solrRequest); reqb.uri(new URI(url + queryParams.toQueryString())); - return new PreparedRequest(reqb, null, null); + return new PreparedRequest(reqb); } private PreparedRequest preparePutOrPost( @@ -322,28 +306,16 @@ private PreparedRequest preparePutOrPost( } HttpRequest.BodyPublisher bodyPublisher; - Future contentWritingFuture = null; - PipedInputStream contentWritingSink = null; + PreparedRequest pReq = new PreparedRequest(reqb); if (contentWriter != null) { boolean success = maybeTryHeadRequest(url); if (!success) { reqb.version(HttpClient.Version.HTTP_1_1); } - final PipedOutputStream source = new PipedOutputStream(); - contentWritingSink = new PipedInputStream(source); - final PipedInputStream sink = contentWritingSink; - bodyPublisher = HttpRequest.BodyPublishers.ofInputStream(() -> sink); - - contentWritingFuture = - executor.submit( - () -> { - try (source) { - contentWriter.write(source); - } catch (Exception e) { - log.error("Cannot write Content Stream", e); - } - }); + bodyPublisher = + HttpRequest.BodyPublishers.ofInputStream( + () -> pReq.beginContentWriting(contentWriter, this.executor)); } else if (streams != null && streams.size() == 1) { boolean success = maybeTryHeadRequest(url); if (!success) { @@ -374,25 +346,60 @@ private PreparedRequest preparePutOrPost( URI uriWithQueryParams = new URI(url + queryParams.toQueryString()); reqb.uri(uriWithQueryParams); - return new PreparedRequest(reqb, contentWritingFuture, contentWritingSink); + return pReq; } protected static class PreparedRequest { - Future contentWritingFuture; - PipedInputStream contentWritingSink; - HttpRequest.Builder reqb; + final HttpRequest.Builder reqb; ResponseParser parserToUse; String url; - PreparedRequest( - HttpRequest.Builder reqb, - Future contentWritingFuture, - PipedInputStream contentWritingSink) { + // Both remain null if the request has no streamed content, or if the body is never requested + // (e.g. the connection failed before sending it). Filled in lazily by + // beginContentWriting once the JDK HttpClient actually requests the body. + volatile PipedInputStream contentWritingSink; + volatile Future contentWritingFuture; + + PreparedRequest(HttpRequest.Builder reqb) { this.reqb = reqb; - this.contentWritingFuture = contentWritingFuture; - this.contentWritingSink = contentWritingSink; + } + + private PipedInputStream beginContentWriting( + RequestWriter.ContentWriter contentWriter, ExecutorService bodyExecutor) { + final PipedOutputStream source = new PipedOutputStream(); + try { + contentWritingSink = new PipedInputStream(source); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + contentWritingFuture = + bodyExecutor.submit( + () -> { + try (source) { + contentWriter.write(source); + } catch (Exception e) { + log.error("Cannot write Content Stream", e); + } + }); + return contentWritingSink; + } + + private void releaseContentWriting() { + if (contentWritingFuture != null) { + contentWritingFuture.cancel(true); + } + // Closing the sink is what unblocks a writer already stuck in the pipe; cancel() alone does + // not. + PipedInputStream sink = contentWritingSink; + if (sink != null) { + try { + sink.close(); + } catch (IOException e) { + log.warn("Could not close content-writing pipe", e); + } + } } } From 0c5768f9fe6b5968e10b93fcc926b2f58a87130a Mon Sep 17 00:00:00 2001 From: David Smiley Date: Thu, 30 Jul 2026 10:04:16 -0400 Subject: [PATCH 5/9] Switch from volatile to synchronized --- .../client/solrj/impl/HttpJdkSolrClient.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java index db810f23b8c..64ae67c3182 100644 --- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java @@ -359,14 +359,14 @@ protected static class PreparedRequest { // Both remain null if the request has no streamed content, or if the body is never requested // (e.g. the connection failed before sending it). Filled in lazily by // beginContentWriting once the JDK HttpClient actually requests the body. - volatile PipedInputStream contentWritingSink; - volatile Future contentWritingFuture; + private PipedInputStream contentWritingSink; + private Future contentWritingFuture; PreparedRequest(HttpRequest.Builder reqb) { this.reqb = reqb; } - private PipedInputStream beginContentWriting( + synchronized PipedInputStream beginContentWriting( RequestWriter.ContentWriter contentWriter, ExecutorService bodyExecutor) { final PipedOutputStream source = new PipedOutputStream(); try { @@ -374,9 +374,11 @@ private PipedInputStream beginContentWriting( } catch (IOException e) { throw new UncheckedIOException(e); } + contentWritingFuture = bodyExecutor.submit( () -> { + // note: doesn't need to synchronize with PreparedRequest.this try (source) { contentWriter.write(source); } catch (Exception e) { @@ -386,16 +388,16 @@ private PipedInputStream beginContentWriting( return contentWritingSink; } - private void releaseContentWriting() { + synchronized void releaseContentWriting() { if (contentWritingFuture != null) { contentWritingFuture.cancel(true); } + // Closing the sink is what unblocks a writer already stuck in the pipe; cancel() alone does // not. - PipedInputStream sink = contentWritingSink; - if (sink != null) { + if (contentWritingSink != null) { try { - sink.close(); + contentWritingSink.close(); } catch (IOException e) { log.warn("Could not close content-writing pipe", e); } From 1de32824767deb33d1e0a4104591350989fcc98a Mon Sep 17 00:00:00 2001 From: Renato Haeberli Date: Fri, 14 Aug 2026 14:50:32 +0200 Subject: [PATCH 6/9] SOLR-18312: adding documentation --- .../modules/upgrade-notes/pages/major-changes-in-solr-10.adoc | 2 ++ .../org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java | 3 +++ 2 files changed, 5 insertions(+) diff --git a/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-10.adoc b/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-10.adoc index 762c5e440d8..f22c5a854ea 100644 --- a/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-10.adoc +++ b/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-10.adoc @@ -95,6 +95,8 @@ When enabled, this escape hatch relaxes both the login-time check and the user c It is intended as a temporary measure while you migrate the affected accounts to stronger passwords, and should be removed once that is done. === SolrJ +`HttpJdkSolrClient` now uses two separate ThreadPoolExecutors, one for the JDK HttpClient (consumer) and one for writing request bodies (producer). +Both are unbounded cached thread pools (maximumPoolSize = Integer.MAX_VALUE), create with ExecutorUtil.newMDCAwareCachedThreadPool. HttpSolrClient returns; this time as a base class for HttpJettySolrClient and HttpJdkSolrClient. Its builder will dynamically detect if solr-jetty is available and use that, otherwise it will use the JDK client. diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java index 64ae67c3182..780746f7e12 100644 --- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java @@ -67,6 +67,9 @@ * Client. This client is targeted for those users who wish to minimize application dependencies. * This client will connect to solr using Http/2 but can seamlessly downgrade to Http/1.1 when * connecting to Solr hosts running on older versions. + * Uses two {@link java.util.concurrent.ThreadPoolExecutor}, one for + * {@link HttpClient} (consumer) and one for writing request bodies (producer). + * Both are unbounded cached thread pools (maximumPoolSize = Integer.MAX_VALUE). */ public class HttpJdkSolrClient extends HttpSolrClient { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); From 534e2da6d58e23db794d0d39252313c58b9e321a Mon Sep 17 00:00:00 2001 From: Renato Haeberli Date: Fri, 14 Aug 2026 18:28:09 +0200 Subject: [PATCH 7/9] SOLR-18312: using the the executor provided on the builder for the httpClient and not for writing the request bodies --- .../pages/major-changes-in-solr-10.adoc | 3 ++- .../client/solrj/impl/HttpJdkSolrClient.java | 25 +++++++++++-------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-10.adoc b/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-10.adoc index f22c5a854ea..47dbbf01985 100644 --- a/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-10.adoc +++ b/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-10.adoc @@ -96,7 +96,8 @@ It is intended as a temporary measure while you migrate the affected accounts to === SolrJ `HttpJdkSolrClient` now uses two separate ThreadPoolExecutors, one for the JDK HttpClient (consumer) and one for writing request bodies (producer). -Both are unbounded cached thread pools (maximumPoolSize = Integer.MAX_VALUE), create with ExecutorUtil.newMDCAwareCachedThreadPool. +Both are unbounded cached thread pools (maximumPoolSize = Integer.MAX_VALUE), created with ExecutorUtil.newMDCAwareCachedThreadPool. +If a Builder.executor is provided, it will be used for the HttpClient only. The one for writing the request bodies cannot be passed in. HttpSolrClient returns; this time as a base class for HttpJettySolrClient and HttpJdkSolrClient. Its builder will dynamically detect if solr-jetty is available and use that, otherwise it will use the JDK client. diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java index 780746f7e12..dbe78db2aad 100644 --- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java @@ -83,15 +83,18 @@ public class HttpJdkSolrClient extends HttpSolrClient { * Executor used to stream (produce) request bodies into the pipe consumed by the JDK HttpClient. * This is the "producer" side and may be supplied by the caller. */ - protected ExecutorService executor; + protected ExecutorService requestBodyExecutor; /** Dedicated executor handed to the JDK HttpClient */ - protected ExecutorService httpClientExecutor; + protected ExecutorService executor; private boolean forceHttp11; private final boolean shutdownExecutor; + /** + * {@link ExecutorService} on {@link HttpJdkSolrClient.Builder} is used for {@link HttpClient} only. + */ protected HttpJdkSolrClient(String serverBaseUrl, HttpJdkSolrClient.Builder builder) { super(serverBaseUrl, builder); HttpClient.Builder httpClientBuilder = HttpClient.newBuilder(); @@ -117,13 +120,14 @@ protected HttpJdkSolrClient(String serverBaseUrl, HttpJdkSolrClient.Builder buil } else { this.executor = ExecutorUtil.newMDCAwareCachedThreadPool( - new SolrNamedThreadFactory(this.getClass().getSimpleName() + "-reqBody")); + new SolrNamedThreadFactory(this.getClass().getSimpleName() + "-http")); this.shutdownExecutor = true; } - this.httpClientExecutor = + httpClientBuilder.executor(this.executor); + + this.requestBodyExecutor = ExecutorUtil.newMDCAwareCachedThreadPool( - new SolrNamedThreadFactory(this.getClass().getSimpleName() + "-http")); - httpClientBuilder.executor(this.httpClientExecutor); + new SolrNamedThreadFactory(this.getClass().getSimpleName() + "-reqBody")); if (builder.shouldUseHttp1_1()) { this.forceHttp11 = true; @@ -318,7 +322,7 @@ private PreparedRequest preparePutOrPost( bodyPublisher = HttpRequest.BodyPublishers.ofInputStream( - () -> pReq.beginContentWriting(contentWriter, this.executor)); + () -> pReq.beginContentWriting(contentWriter, this.requestBodyExecutor)); } else if (streams != null && streams.size() == 1) { boolean success = maybeTryHeadRequest(url); if (!success) { @@ -559,10 +563,9 @@ public void close() throws IOException { } executor = null; - // The http client executor is always created and owned by this instance. - if (httpClientExecutor != null) { - ExecutorUtil.shutdownAndAwaitTermination(httpClientExecutor); - httpClientExecutor = null; + if (requestBodyExecutor != null) { + ExecutorUtil.shutdownAndAwaitTermination(requestBodyExecutor); + requestBodyExecutor = null; } assert ObjectReleaseTracker.release(this); From 70f6016fdad1924d4bec9217b446fdb871ea71a4 Mon Sep 17 00:00:00 2001 From: Renato Haeberli Date: Fri, 14 Aug 2026 22:12:42 +0200 Subject: [PATCH 8/9] Update solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-10.adoc Co-authored-by: David Smiley --- .../modules/upgrade-notes/pages/major-changes-in-solr-10.adoc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-10.adoc b/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-10.adoc index 47dbbf01985..228ce2f5d1e 100644 --- a/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-10.adoc +++ b/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-10.adoc @@ -95,9 +95,7 @@ When enabled, this escape hatch relaxes both the login-time check and the user c It is intended as a temporary measure while you migrate the affected accounts to stronger passwords, and should be removed once that is done. === SolrJ -`HttpJdkSolrClient` now uses two separate ThreadPoolExecutors, one for the JDK HttpClient (consumer) and one for writing request bodies (producer). -Both are unbounded cached thread pools (maximumPoolSize = Integer.MAX_VALUE), created with ExecutorUtil.newMDCAwareCachedThreadPool. -If a Builder.executor is provided, it will be used for the HttpClient only. The one for writing the request bodies cannot be passed in. +The `HttpJdkSolrClient` no longer has default thread/connection limits, at least not beyond what the JDK's client intrinsically does. HttpSolrClient returns; this time as a base class for HttpJettySolrClient and HttpJdkSolrClient. Its builder will dynamically detect if solr-jetty is available and use that, otherwise it will use the JDK client. From f55d7416dc17e280f1b43f7d08f0b41d6ae7fa8e Mon Sep 17 00:00:00 2001 From: Renato Haeberli Date: Sat, 15 Aug 2026 15:01:12 +0200 Subject: [PATCH 9/9] SOLR-18312: cleaning up java doc and thread name --- .../org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java index dbe78db2aad..8f2641d7b02 100644 --- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.java @@ -81,7 +81,6 @@ public class HttpJdkSolrClient extends HttpSolrClient { /** * Executor used to stream (produce) request bodies into the pipe consumed by the JDK HttpClient. - * This is the "producer" side and may be supplied by the caller. */ protected ExecutorService requestBodyExecutor; @@ -120,7 +119,7 @@ protected HttpJdkSolrClient(String serverBaseUrl, HttpJdkSolrClient.Builder buil } else { this.executor = ExecutorUtil.newMDCAwareCachedThreadPool( - new SolrNamedThreadFactory(this.getClass().getSimpleName() + "-http")); + new SolrNamedThreadFactory(this.getClass().getSimpleName())); this.shutdownExecutor = true; } httpClientBuilder.executor(this.executor);