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

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


The following commit(s) were added to refs/heads/main by this push:
     new 704f2fe3f72 Revert "SOLR-17541: LBSolrClient implementations should 
agree on 'getClient()' semantics (#2899)"
704f2fe3f72 is described below

commit 704f2fe3f72fd2402d14d16c2f9d6e866869723d
Author: jdyer1 <jd...@apache.org>
AuthorDate: Mon Dec 23 17:41:16 2024 -0600

    Revert "SOLR-17541: LBSolrClient implementations should agree on 
'getClient()' semantics (#2899)"
    
    This reverts commit d3e57aad8259f4b9c574fbff5cb5f6d112bef85a.
---
 solr/CHANGES.txt                                   |  14 +-
 .../java/org/apache/solr/cloud/ZkController.java   |  11 +-
 .../apache/solr/core/HttpSolrClientProvider.java   |  13 +-
 .../solr/handler/component/HttpShardHandler.java   |   2 +-
 .../handler/component/HttpShardHandlerFactory.java |  15 +-
 .../solr/security/HttpClientBuilderPlugin.java     |   5 -
 .../solr/security/PKIAuthenticationPlugin.java     |  12 +-
 .../src/test/org/apache/solr/cli/PostToolTest.java |   1 -
 .../test/org/apache/solr/cloud/OverseerTest.java   |   9 +-
 .../solr/client/solrj/SolrClientFunction.java      |   6 +-
 .../client/solrj/impl/CloudHttp2SolrClient.java    |  45 +++-
 .../solr/client/solrj/impl/Http2SolrClient.java    |  36 +--
 .../solr/client/solrj/impl/HttpJdkSolrClient.java  |  24 +-
 .../solr/client/solrj/impl/LBHttp2SolrClient.java  | 116 ++++------
 .../solr/client/solrj/impl/LBSolrClient.java       |  32 ++-
 .../impl/CloudHttp2SolrClientBuilderTest.java      |  56 ++++-
 .../client/solrj/impl/HttpJdkSolrClientTest.java   |  20 ++
 .../impl/LBHttp2SolrClientIntegrationTest.java     |  28 ++-
 .../client/solrj/impl/LBHttp2SolrClientTest.java   | 247 +++++++++------------
 19 files changed, 348 insertions(+), 344 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index a135ecaa539..a844220f2fc 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -34,10 +34,6 @@ Improvements
 
 * SOLR-17516: `LBHttp2SolrClient` is now generic, adding support for 
`HttpJdkSolrClient`. (James Dyer)
 
-* SOLR-17541: `LBHttp2SolrClient` now maintains a separate internal/delegate 
client per Solr Base URL.  Both `LBHttp2SolrClient` and `CloudHttp2SolrClient`
-  always create and manage these internal clients.  The ability for callers to 
provide a pre-built client is removed.  Callers may specify the internal client
-  details by providing an instance of either `Http2SolrClient.Builder` or 
`HttpJdkSolrClient.Builder`. (James Dyer)
-
 Optimizations
 ---------------------
 * SOLR-17568: The CLI bin/solr export tool now contacts the appropriate nodes 
directly for data instead of proxying through one.
@@ -106,11 +102,6 @@ Deprecation Removals
 
 * SOLR-17540: Removed the Hadoop Auth module, and thus Kerberos authentication 
and other exotic options. (Eric Pugh)
 
-* SOLR-17541: Removed `CloudHttp2SolrClient.Builder#withHttpClient` in favor 
of `CloudHttp2SolrClient.Builder#withInternalClientBuilder`.
-  The constructor on `LBHttp2SolrClient.Builder` that took an instance of 
`HttpSolrClientBase` is updated to instead take an instance of
-  `HttpSolrClientBuilderBase`.  Renamed 
`LBHttp2SolrClient.Builder#withListenerFactory` to 
`LBHttp2SolrClient.Builder#withListenerFactories`
-  (James Dyer)
-
 Dependency Upgrades
 ---------------------
 (No changes)
@@ -159,10 +150,7 @@ New Features
 
 Improvements
 ---------------------
-* SOLR-17541: Deprecate `CloudHttp2SolrClient.Builder#withHttpClient` in favor 
of
-  `CloudHttp2SolrClient.Builder#withInternalClientBuilder`.
-   Deprecate `LBHttp2SolrClient.Builder#withListenerFactory` in favor of
-   `LBHttp2SolrClient.Builder#withListenerFactories` (James Dyer)
+(No changes)
 
 Optimizations
 ---------------------
diff --git a/solr/core/src/java/org/apache/solr/cloud/ZkController.java 
b/solr/core/src/java/org/apache/solr/cloud/ZkController.java
index 5a890121a43..7dd30a14d24 100644
--- a/solr/core/src/java/org/apache/solr/cloud/ZkController.java
+++ b/solr/core/src/java/org/apache/solr/cloud/ZkController.java
@@ -197,6 +197,9 @@ public class ZkController implements Closeable {
   public final ZkStateReader zkStateReader;
   private SolrCloudManager cloudManager;
 
+  // only for internal usage
+  private Http2SolrClient http2SolrClient;
+
   private CloudHttp2SolrClient cloudSolrClient;
 
   private final String zkServerAddress; // example: 127.0.0.1:54062/solr
@@ -751,6 +754,7 @@ public class ZkController implements Closeable {
       sysPropsCacher.close();
       customThreadPool.execute(() -> IOUtils.closeQuietly(cloudManager));
       customThreadPool.execute(() -> IOUtils.closeQuietly(cloudSolrClient));
+      customThreadPool.execute(() -> IOUtils.closeQuietly(http2SolrClient));
 
       try {
         try {
@@ -846,14 +850,15 @@ public class ZkController implements Closeable {
       if (cloudManager != null) {
         return cloudManager;
       }
-      var httpSolrClientBuilder =
+      http2SolrClient =
           new Http2SolrClient.Builder()
               .withHttpClient(cc.getDefaultHttpSolrClient())
               .withIdleTimeout(30000, TimeUnit.MILLISECONDS)
-              .withConnectionTimeout(15000, TimeUnit.MILLISECONDS);
+              .withConnectionTimeout(15000, TimeUnit.MILLISECONDS)
+              .build();
       cloudSolrClient =
           new CloudHttp2SolrClient.Builder(new 
ZkClientClusterStateProvider(zkStateReader))
-              .withInternalClientBuilder(httpSolrClientBuilder)
+              .withHttpClient(http2SolrClient)
               .build();
       cloudManager = new SolrClientCloudManager(cloudSolrClient, 
cc.getObjectCache());
       cloudManager.getClusterStateProvider().connect();
diff --git 
a/solr/core/src/java/org/apache/solr/core/HttpSolrClientProvider.java 
b/solr/core/src/java/org/apache/solr/core/HttpSolrClientProvider.java
index e9631b26d1f..2bf25a896f6 100644
--- a/solr/core/src/java/org/apache/solr/core/HttpSolrClientProvider.java
+++ b/solr/core/src/java/org/apache/solr/core/HttpSolrClientProvider.java
@@ -16,6 +16,7 @@
  */
 package org.apache.solr.core;
 
+import java.util.List;
 import java.util.concurrent.TimeUnit;
 import org.apache.solr.client.solrj.impl.Http2SolrClient;
 import org.apache.solr.common.util.IOUtils;
@@ -36,24 +37,22 @@ final class HttpSolrClientProvider implements AutoCloseable 
{
 
   private final Http2SolrClient httpSolrClient;
 
-  private final Http2SolrClient.Builder httpSolrClientBuilder;
-
   private final InstrumentedHttpListenerFactory trackHttpSolrMetrics;
 
   HttpSolrClientProvider(UpdateShardHandlerConfig cfg, SolrMetricsContext 
parentContext) {
     trackHttpSolrMetrics = new 
InstrumentedHttpListenerFactory(getNameStrategy(cfg));
     initializeMetrics(parentContext);
 
-    this.httpSolrClientBuilder =
-        new Http2SolrClient.Builder().addListenerFactory(trackHttpSolrMetrics);
+    Http2SolrClient.Builder httpClientBuilder =
+        new 
Http2SolrClient.Builder().withListenerFactory(List.of(trackHttpSolrMetrics));
 
     if (cfg != null) {
-      httpSolrClientBuilder
+      httpClientBuilder
           .withConnectionTimeout(cfg.getDistributedConnectionTimeout(), 
TimeUnit.MILLISECONDS)
           .withIdleTimeout(cfg.getDistributedSocketTimeout(), 
TimeUnit.MILLISECONDS)
           .withMaxConnectionsPerHost(cfg.getMaxUpdateConnectionsPerHost());
     }
-    httpSolrClient = httpSolrClientBuilder.build();
+    httpSolrClient = httpClientBuilder.build();
   }
 
   private InstrumentedHttpListenerFactory.NameStrategy getNameStrategy(
@@ -77,7 +76,7 @@ final class HttpSolrClientProvider implements AutoCloseable {
   }
 
   void setSecurityBuilder(HttpClientBuilderPlugin builder) {
-    builder.setup(httpSolrClientBuilder, httpSolrClient);
+    builder.setup(httpSolrClient);
   }
 
   @Override
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 320a5fe70d7..7592eed86fc 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
@@ -114,7 +114,7 @@ public class HttpShardHandler extends ShardHandler {
   protected AtomicInteger pending;
 
   private final Map<String, List<String>> shardToURLs;
-  protected LBHttp2SolrClient<Http2SolrClient.Builder> lbClient;
+  protected LBHttp2SolrClient<Http2SolrClient> lbClient;
 
   public HttpShardHandler(HttpShardHandlerFactory httpShardHandlerFactory) {
     this.httpShardHandlerFactory = httpShardHandlerFactory;
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 1437dee63ea..ac7dc0cf8e0 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
@@ -83,9 +83,8 @@ public class HttpShardHandlerFactory extends 
ShardHandlerFactory
   protected ExecutorService commExecutor;
 
   protected volatile Http2SolrClient defaultClient;
-  protected Http2SolrClient.Builder httpSolrClientBuilder;
   protected InstrumentedHttpListenerFactory httpListenerFactory;
-  protected LBHttp2SolrClient<Http2SolrClient.Builder> loadbalancer;
+  protected LBHttp2SolrClient<Http2SolrClient> loadbalancer;
 
   int corePoolSize = 0;
   int maximumPoolSize = Integer.MAX_VALUE;
@@ -306,16 +305,16 @@ public class HttpShardHandlerFactory extends 
ShardHandlerFactory
             sb);
     int soTimeout =
         getParameter(args, HttpClientUtil.PROP_SO_TIMEOUT, 
HttpClientUtil.DEFAULT_SO_TIMEOUT, sb);
-    this.httpSolrClientBuilder =
+
+    this.defaultClient =
         new Http2SolrClient.Builder()
             .withConnectionTimeout(connectionTimeout, TimeUnit.MILLISECONDS)
             .withIdleTimeout(soTimeout, TimeUnit.MILLISECONDS)
             .withExecutor(commExecutor)
             .withMaxConnectionsPerHost(maxConnectionsPerHost)
-            .addListenerFactory(this.httpListenerFactory);
-    this.defaultClient = httpSolrClientBuilder.build();
-
-    this.loadbalancer = new 
LBHttp2SolrClient.Builder<>(httpSolrClientBuilder).build();
+            .build();
+    this.defaultClient.addListenerFactory(this.httpListenerFactory);
+    this.loadbalancer = new 
LBHttp2SolrClient.Builder<Http2SolrClient>(defaultClient).build();
 
     initReplicaListTransformers(getParameter(args, "replicaRouting", null, 
sb));
 
@@ -325,7 +324,7 @@ public class HttpShardHandlerFactory extends 
ShardHandlerFactory
   @Override
   public void setSecurityBuilder(HttpClientBuilderPlugin clientBuilderPlugin) {
     if (clientBuilderPlugin != null) {
-      clientBuilderPlugin.setup(httpSolrClientBuilder, defaultClient);
+      clientBuilderPlugin.setup(defaultClient);
     }
   }
 
diff --git 
a/solr/core/src/java/org/apache/solr/security/HttpClientBuilderPlugin.java 
b/solr/core/src/java/org/apache/solr/security/HttpClientBuilderPlugin.java
index 206fb3d0886..b43d5f22190 100644
--- a/solr/core/src/java/org/apache/solr/security/HttpClientBuilderPlugin.java
+++ b/solr/core/src/java/org/apache/solr/security/HttpClientBuilderPlugin.java
@@ -34,9 +34,4 @@ public interface HttpClientBuilderPlugin {
   public SolrHttpClientBuilder getHttpClientBuilder(SolrHttpClientBuilder 
builder);
 
   public default void setup(Http2SolrClient client) {}
-
-  /** TODO: Ideally, we only pass the builder here. */
-  public default void setup(Http2SolrClient.Builder builder, Http2SolrClient 
client) {
-    setup(client);
-  }
 }
diff --git 
a/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java 
b/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java
index 93ecdcc9d68..b1f6e6b6eed 100644
--- a/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java
+++ b/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java
@@ -376,11 +376,6 @@ public class PKIAuthenticationPlugin extends 
AuthenticationPlugin
 
   @Override
   public void setup(Http2SolrClient client) {
-    setup(null, client);
-  }
-
-  @Override
-  public void setup(Http2SolrClient.Builder builder, Http2SolrClient client) {
     final HttpListenerFactory.RequestResponseListener listener =
         new HttpListenerFactory.RequestResponseListener() {
           private static final String CACHED_REQUEST_USER_KEY = 
"cachedRequestUser";
@@ -436,12 +431,7 @@ public class PKIAuthenticationPlugin extends 
AuthenticationPlugin
                 (String) request.getAttributes().get(CACHED_REQUEST_USER_KEY));
           }
         };
-    if (client != null) {
-      client.addListenerFactory(() -> listener);
-    }
-    if (builder != null) {
-      builder.addListenerFactory(() -> listener);
-    }
+    client.addListenerFactory(() -> listener);
   }
 
   @Override
diff --git a/solr/core/src/test/org/apache/solr/cli/PostToolTest.java 
b/solr/core/src/test/org/apache/solr/cli/PostToolTest.java
index 9eb3e783a2d..758ae9ccd51 100644
--- a/solr/core/src/test/org/apache/solr/cli/PostToolTest.java
+++ b/solr/core/src/test/org/apache/solr/cli/PostToolTest.java
@@ -76,7 +76,6 @@ public class PostToolTest extends SolrCloudTestCase {
 
     withBasicAuth(CollectionAdminRequest.createCollection(collection, "conf1", 
1, 1, 0, 0))
         .processAndWait(cluster.getSolrClient(), 10);
-    waitForState("creating", collection, activeClusterShape(1, 1));
 
     File jsonDoc = File.createTempFile("temp", ".json");
 
diff --git a/solr/core/src/test/org/apache/solr/cloud/OverseerTest.java 
b/solr/core/src/test/org/apache/solr/cloud/OverseerTest.java
index e611902898f..bad8d58d021 100644
--- a/solr/core/src/test/org/apache/solr/cloud/OverseerTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/OverseerTest.java
@@ -1945,16 +1945,17 @@ public class OverseerTest extends SolrTestCaseJ4 {
   }
 
   private SolrCloudManager getCloudDataProvider(ZkStateReader zkStateReader) {
-    var httpSolrClientBuilder =
+    var httpSolrClient =
         new Http2SolrClient.Builder()
             .withIdleTimeout(30000, TimeUnit.MILLISECONDS)
-            .withConnectionTimeout(15000, TimeUnit.MILLISECONDS);
+            .withConnectionTimeout(15000, TimeUnit.MILLISECONDS)
+            .build();
     var cloudSolrClient =
         new CloudHttp2SolrClient.Builder(new 
ZkClientClusterStateProvider(zkStateReader))
-            .withInternalClientBuilder(httpSolrClientBuilder)
+            .withHttpClient(httpSolrClient)
             .build();
     solrClients.add(cloudSolrClient);
-    solrClients.add(httpSolrClientBuilder.build());
+    solrClients.add(httpSolrClient);
     SolrClientCloudManager sccm = new SolrClientCloudManager(cloudSolrClient, 
null);
     sccm.getClusterStateProvider().connect();
     return sccm;
diff --git 
a/solr/solrj/src/java/org/apache/solr/client/solrj/SolrClientFunction.java 
b/solr/solrj/src/java/org/apache/solr/client/solrj/SolrClientFunction.java
index 68246001a40..0adb49471dc 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/SolrClientFunction.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/SolrClientFunction.java
@@ -18,11 +18,7 @@ package org.apache.solr.client.solrj;
 
 import java.io.IOException;
 
-/**
- * A lambda intended for invoking SolrClient operations
- *
- * @lucene.experimental
- */
+/** A lambda intended for invoking SolrClient operations */
 @FunctionalInterface
 public interface SolrClientFunction<C extends SolrClient, R> {
   R apply(C c) throws IOException, SolrServerException;
diff --git 
a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/CloudHttp2SolrClient.java
 
b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/CloudHttp2SolrClient.java
index 18aa318f8ba..ade1ebe433f 100644
--- 
a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/CloudHttp2SolrClient.java
+++ 
b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/CloudHttp2SolrClient.java
@@ -40,8 +40,9 @@ import org.apache.solr.common.SolrException;
 public class CloudHttp2SolrClient extends CloudSolrClient {
 
   private final ClusterStateProvider stateProvider;
-  private final LBHttp2SolrClient<Http2SolrClient.Builder> lbClient;
+  private final LBHttp2SolrClient<Http2SolrClient> lbClient;
   private final Http2SolrClient myClient;
+  private final boolean clientIsInternal;
 
   /**
    * Create a new client object that connects to Zookeeper and is always aware 
of the SolrCloud
@@ -53,8 +54,8 @@ public class CloudHttp2SolrClient extends CloudSolrClient {
    */
   protected CloudHttp2SolrClient(Builder builder) {
     super(builder.shardLeadersOnly, builder.parallelUpdates, 
builder.directUpdatesToLeadersOnly);
-    var httpSolrClientBuilder = createOrGetHttpClientBuilder(builder);
-    this.myClient = httpSolrClientBuilder.build();
+    this.clientIsInternal = builder.httpClient == null;
+    this.myClient = createOrGetHttpClientFromBuilder(builder);
     this.stateProvider = createClusterStateProvider(builder);
     this.retryExpiryTimeNano = builder.retryExpiryTimeNano;
     this.defaultCollection = builder.defaultCollection;
@@ -72,14 +73,16 @@ public class CloudHttp2SolrClient extends CloudSolrClient {
     // locks.
     this.locks = objectList(builder.parallelCacheRefreshesLocks);
 
-    this.lbClient = new 
LBHttp2SolrClient.Builder<>(httpSolrClientBuilder).build();
+    this.lbClient = new 
LBHttp2SolrClient.Builder<Http2SolrClient>(myClient).build();
   }
 
-  private Http2SolrClient.Builder createOrGetHttpClientBuilder(Builder 
builder) {
-    if (builder.internalClientBuilder != null) {
-      return builder.internalClientBuilder;
+  private Http2SolrClient createOrGetHttpClientFromBuilder(Builder builder) {
+    if (builder.httpClient != null) {
+      return builder.httpClient;
+    } else if (builder.internalClientBuilder != null) {
+      return builder.internalClientBuilder.build();
     } else {
-      return new Http2SolrClient.Builder();
+      return new Http2SolrClient.Builder().build();
     }
   }
 
@@ -126,7 +129,7 @@ public class CloudHttp2SolrClient extends CloudSolrClient {
 
   private void closeMyClientIfNeeded() {
     try {
-      if (myClient != null) {
+      if (clientIsInternal && myClient != null) {
         myClient.close();
       }
     } catch (Exception e) {
@@ -145,7 +148,7 @@ public class CloudHttp2SolrClient extends CloudSolrClient {
   }
 
   @Override
-  public LBHttp2SolrClient<Http2SolrClient.Builder> getLbClient() {
+  public LBHttp2SolrClient<Http2SolrClient> getLbClient() {
     return lbClient;
   }
 
@@ -168,6 +171,7 @@ public class CloudHttp2SolrClient extends CloudSolrClient {
     protected Collection<String> zkHosts = new ArrayList<>();
     protected List<String> solrUrls = new ArrayList<>();
     protected String zkChroot;
+    protected Http2SolrClient httpClient;
     protected boolean shardLeadersOnly = true;
     protected boolean directUpdatesToLeadersOnly = false;
     protected boolean parallelUpdates = true;
@@ -400,6 +404,23 @@ public class CloudHttp2SolrClient extends CloudSolrClient {
       return this;
     }
 
+    /**
+     * Set the internal http client.
+     *
+     * <p>Note: closing the httpClient instance is at the responsibility of 
the caller.
+     *
+     * @param httpClient http client
+     * @return this
+     */
+    public Builder withHttpClient(Http2SolrClient httpClient) {
+      if (this.internalClientBuilder != null) {
+        throw new IllegalStateException(
+            "The builder can't accept an httpClient AND an 
internalClientBuilder, only one of those can be provided");
+      }
+      this.httpClient = httpClient;
+      return this;
+    }
+
     /**
      * If provided, the CloudHttp2SolrClient will build it's internal 
Http2SolrClient using this
      * builder (instead of the empty default one). Providing this builder 
allows users to configure
@@ -409,6 +430,10 @@ public class CloudHttp2SolrClient extends CloudSolrClient {
      * @return this
      */
     public Builder withInternalClientBuilder(Http2SolrClient.Builder 
internalClientBuilder) {
+      if (this.httpClient != null) {
+        throw new IllegalStateException(
+            "The builder can't accept an httpClient AND an 
internalClientBuilder, only one of those can be provided");
+      }
       this.internalClientBuilder = internalClientBuilder;
       return this;
     }
diff --git 
a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/Http2SolrClient.java 
b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/Http2SolrClient.java
index fddc4e2daa6..fb2eb1a123f 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/Http2SolrClient.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/Http2SolrClient.java
@@ -140,7 +140,9 @@ public class Http2SolrClient extends HttpSolrClientBase {
       this.httpClient = createHttpClient(builder);
       this.closeClient = true;
     }
-    this.listenerFactory.addAll(builder.listenerFactories);
+    if (builder.listenerFactory != null) {
+      this.listenerFactory.addAll(builder.listenerFactory);
+    }
     updateDefaultMimeTypeForParser();
 
     
this.httpClient.setFollowRedirects(Boolean.TRUE.equals(builder.followRedirects));
@@ -569,7 +571,6 @@ public class Http2SolrClient extends HttpSolrClientBase {
    * @param clientFunction a Function that consumes a Http2SolrClient and 
returns an arbitrary value
    * @return the value returned after invoking 'clientFunction'
    * @param <R> the type returned by the provided function (and by this method)
-   * @lucene.experimental
    */
   public <R> R requestWithBaseUrl(
       String baseUrl, SolrClientFunction<Http2SolrClient, R> clientFunction)
@@ -905,7 +906,7 @@ public class Http2SolrClient extends HttpSolrClientBase {
 
     protected Long keyStoreReloadIntervalSecs;
 
-    private List<HttpListenerFactory> listenerFactories = new ArrayList<>(0);
+    private List<HttpListenerFactory> listenerFactory;
 
     public Builder() {
       super();
@@ -931,27 +932,8 @@ public class Http2SolrClient extends HttpSolrClientBase {
       this.baseSolrUrl = baseSolrUrl;
     }
 
-    /**
-     * specify a listener factory, which will be appended to any existing 
values.
-     *
-     * @param listenerFactory a HttpListenerFactory
-     * @return This Builder
-     */
-    public Http2SolrClient.Builder addListenerFactory(HttpListenerFactory 
listenerFactory) {
-      this.listenerFactories.add(listenerFactory);
-      return this;
-    }
-
-    /**
-     * Specify listener factories, which will replace any existing values.
-     *
-     * @param listenerFactories a list of HttpListenerFactory instances
-     * @return This Builder
-     */
-    public Http2SolrClient.Builder withListenerFactories(
-        List<HttpListenerFactory> listenerFactories) {
-      this.listenerFactories.clear();
-      this.listenerFactories.addAll(listenerFactories);
+    public Http2SolrClient.Builder 
withListenerFactory(List<HttpListenerFactory> listenerFactory) {
+      this.listenerFactory = listenerFactory;
       return this;
     }
 
@@ -1127,9 +1109,9 @@ public class Http2SolrClient extends HttpSolrClientBase {
       if (this.urlParamNames == null) {
         this.urlParamNames = http2SolrClient.urlParamNames;
       }
-      if (this.listenerFactories.isEmpty()) {
-        this.listenerFactories.clear();
-        http2SolrClient.listenerFactory.forEach(this.listenerFactories::add);
+      if (this.listenerFactory == null) {
+        this.listenerFactory = new ArrayList<HttpListenerFactory>();
+        http2SolrClient.listenerFactory.forEach(this.listenerFactory::add);
       }
       if (this.executor == null) {
         this.executor = http2SolrClient.executor;
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 c26d13606b6..1127b3fd1a1 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
@@ -138,7 +138,7 @@ public class HttpJdkSolrClient extends HttpSolrClientBase {
   public CompletableFuture<NamedList<Object>> requestAsync(
       final SolrRequest<?> solrRequest, String collection) {
     try {
-      PreparedRequest pReq = prepareRequest(solrRequest, collection);
+      PreparedRequest pReq = prepareRequest(solrRequest, collection, null);
       return httpClient
           .sendAsync(pReq.reqb.build(), 
HttpResponse.BodyHandlers.ofInputStream())
           .thenApply(
@@ -157,10 +157,10 @@ public class HttpJdkSolrClient extends HttpSolrClientBase 
{
     }
   }
 
-  @Override
-  public NamedList<Object> request(SolrRequest<?> solrRequest, String 
collection)
+  protected NamedList<Object> requestWithBaseUrl(
+      String baseUrl, SolrRequest<?> solrRequest, String collection)
       throws SolrServerException, IOException {
-    PreparedRequest pReq = prepareRequest(solrRequest, collection);
+    PreparedRequest pReq = prepareRequest(solrRequest, collection, baseUrl);
     HttpResponse<InputStream> response = null;
     try {
       response = httpClient.send(pReq.reqb.build(), 
HttpResponse.BodyHandlers.ofInputStream());
@@ -192,13 +192,25 @@ public class HttpJdkSolrClient extends HttpSolrClientBase 
{
     }
   }
 
-  private PreparedRequest prepareRequest(SolrRequest<?> solrRequest, String 
collection)
+  @Override
+  public NamedList<Object> request(SolrRequest<?> solrRequest, String 
collection)
+      throws SolrServerException, IOException {
+    return requestWithBaseUrl(null, solrRequest, collection);
+  }
+
+  private PreparedRequest prepareRequest(
+      SolrRequest<?> solrRequest, String collection, String overrideBaseUrl)
       throws SolrServerException, IOException {
     checkClosed();
     if (ClientUtils.shouldApplyDefaultCollection(collection, solrRequest)) {
       collection = defaultCollection;
     }
-    String url = getRequestUrl(solrRequest, collection);
+    String url;
+    if (overrideBaseUrl != null) {
+      url = ClientUtils.buildRequestUrl(solrRequest, overrideBaseUrl, 
collection);
+    } else {
+      url = getRequestUrl(solrRequest, collection);
+    }
     ResponseParser parserToUse = responseParser(solrRequest);
     ModifiableSolrParams queryParams = initializeSolrParams(solrRequest, 
parserToUse);
     var reqb = HttpRequest.newBuilder();
diff --git 
a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/LBHttp2SolrClient.java 
b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/LBHttp2SolrClient.java
index 4c0d46b13db..2c926a26261 100644
--- 
a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/LBHttp2SolrClient.java
+++ 
b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/LBHttp2SolrClient.java
@@ -23,25 +23,22 @@ import java.net.ConnectException;
 import java.net.SocketException;
 import java.net.SocketTimeoutException;
 import java.util.Arrays;
-import java.util.Collections;
-import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
 import org.apache.solr.client.solrj.ResponseParser;
+import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.request.IsUpdateRequest;
 import org.apache.solr.client.solrj.request.RequestWriter;
 import org.apache.solr.common.SolrException;
-import org.apache.solr.common.util.IOUtils;
 import org.apache.solr.common.util.NamedList;
 import org.slf4j.MDC;
 
 /**
- * This "LoadBalanced Http Solr Client" is a load balancer for multiple Http 
Solr Clients. This is
- * useful when you have multiple Solr endpoints and requests need to be Load 
Balanced among them.
+ * This "LoadBalanced Http Solr Client" is a load balancing wrapper around a 
Http Solr Client. This
+ * is useful when you have multiple Solr endpoints and requests need to be 
Load Balanced among them.
  *
  * <p>Do <b>NOT</b> use this class for indexing in leader/follower scenarios 
since documents must be
  * sent to the correct leader; no inter-node routing is done.
@@ -59,7 +56,7 @@ import org.slf4j.MDC;
  * <blockquote>
  *
  * <pre>
- * SolrClient client = new LBHttp2SolrClient.Builder(http2SolrClientBuilder,
+ * SolrClient client = new LBHttp2SolrClient.Builder(http2SolrClient,
  *         new LBSolrClient.Endpoint("http://host1:8080/solr";), new 
LBSolrClient.Endpoint("http://host2:8080/solr";))
  *     .build();
  * </pre>
@@ -72,7 +69,7 @@ import org.slf4j.MDC;
  * <blockquote>
  *
  * <pre>
- * SolrClient client = new LBHttp2SolrClient.Builder(http2SolrClientBuilder,
+ * SolrClient client = new LBHttp2SolrClient.Builder(http2SolrClient,
  *         new LBSolrClient.Endpoint("http://host1:8080/solr";, "coreA"),
  *         new LBSolrClient.Endpoint("http://host2:8080/solr";, "coreB"))
  *     .build();
@@ -97,63 +94,35 @@ import org.slf4j.MDC;
  *
  * @since solr 8.0
  */
-public class LBHttp2SolrClient<B extends HttpSolrClientBuilderBase<?, ?>> 
extends LBSolrClient {
+public class LBHttp2SolrClient<C extends HttpSolrClientBase> extends 
LBSolrClient {
 
-  private final Map<String, HttpSolrClientBase> urlToClient;
-  private final Set<String> urlParamNames;
-
-  // must synchronize on this when building
-  private final HttpSolrClientBuilderBase<?, ?> solrClientBuilder;
+  protected final C solrClient;
 
+  @SuppressWarnings("unchecked")
   private LBHttp2SolrClient(Builder<?> builder) {
     super(Arrays.asList(builder.solrEndpoints));
-    this.solrClientBuilder = builder.solrClientBuilder;
-
+    this.solrClient = (C) builder.solrClient;
     this.aliveCheckIntervalMillis = builder.aliveCheckIntervalMillis;
     this.defaultCollection = builder.defaultCollection;
-
-    if (builder.solrClientBuilder.urlParamNames == null) {
-      this.urlParamNames = Collections.emptySet();
-    } else {
-      this.urlParamNames = Set.copyOf(builder.solrClientBuilder.urlParamNames);
-    }
-
-    this.urlToClient = new ConcurrentHashMap<>();
-    for (LBSolrClient.Endpoint endpoint : builder.solrEndpoints) {
-      getClient(endpoint);
-    }
   }
 
   @Override
-  protected HttpSolrClientBase getClient(final Endpoint endpoint) {
-    return urlToClient.computeIfAbsent(
-        endpoint.getBaseUrl(),
-        url -> {
-          synchronized (solrClientBuilder) {
-            solrClientBuilder.baseSolrUrl = url;
-            return solrClientBuilder.build();
-          }
-        });
+  protected SolrClient getClient(Endpoint endpoint) {
+    return solrClient;
   }
 
   @Override
   public ResponseParser getParser() {
-    return urlToClient.isEmpty() ? null : 
urlToClient.values().iterator().next().getParser();
+    return solrClient.getParser();
   }
 
   @Override
   public RequestWriter getRequestWriter() {
-    return urlToClient.isEmpty() ? null : 
urlToClient.values().iterator().next().getRequestWriter();
+    return solrClient.getRequestWriter();
   }
 
   public Set<String> getUrlParamNames() {
-    return urlParamNames;
-  }
-
-  @Override
-  public void close() {
-    urlToClient.values().forEach(IOUtils::closeQuietly);
-    super.close();
+    return solrClient.getUrlParamNames();
   }
 
   /**
@@ -241,18 +210,23 @@ public class LBHttp2SolrClient<B extends 
HttpSolrClientBuilderBase<?, ?>> extend
       RetryListener listener) {
     String baseUrl = endpoint.toString();
     rsp.server = baseUrl;
-    final var client = getClient(endpoint);
-    CompletableFuture<NamedList<Object>> future =
-        client.requestAsync(req.getRequest(), endpoint.getCore());
-    future.whenComplete(
-        (result, throwable) -> {
-          if (!future.isCompletedExceptionally()) {
-            onSuccessfulRequest(result, endpoint, rsp, isZombie, listener);
-          } else if (!future.isCancelled()) {
-            onFailedRequest(throwable, endpoint, isNonRetryable, isZombie, 
listener);
-          }
-        });
-    return future;
+    final var client = (Http2SolrClient) getClient(endpoint);
+    try {
+      CompletableFuture<NamedList<Object>> future =
+          client.requestWithBaseUrl(baseUrl, (c) -> 
c.requestAsync(req.getRequest()));
+      future.whenComplete(
+          (result, throwable) -> {
+            if (!future.isCompletedExceptionally()) {
+              onSuccessfulRequest(result, endpoint, rsp, isZombie, listener);
+            } else if (!future.isCancelled()) {
+              onFailedRequest(throwable, endpoint, isNonRetryable, isZombie, 
listener);
+            }
+          });
+      return future;
+    } catch (SolrServerException | IOException e) {
+      // Unreachable, since 'requestWithBaseUrl' above is running the request 
asynchronously
+      throw new RuntimeException(e);
+    }
   }
 
   private void onSuccessfulRequest(
@@ -316,28 +290,16 @@ public class LBHttp2SolrClient<B extends 
HttpSolrClientBuilderBase<?, ?>> extend
     }
   }
 
-  public static class Builder<B extends HttpSolrClientBuilderBase<?, ?>> {
-
-    private final B solrClientBuilder;
+  public static class Builder<C extends HttpSolrClientBase> {
 
+    private final C solrClient;
     private final LBSolrClient.Endpoint[] solrEndpoints;
     private long aliveCheckIntervalMillis =
         TimeUnit.MILLISECONDS.convert(60, TimeUnit.SECONDS); // 1 minute 
between checks
     protected String defaultCollection;
 
-    /**
-     * Use this Builder to configure an LBHttp2SolrClient. The passed-in Solr 
Client Builder will be
-     * used to generate an internal client per Endpoint.
-     *
-     * <p>Implementation Note: LBHttp2SolrClient will modify the passed-in 
Builder's {@link
-     * HttpSolrClientBuilderBase#baseSolrUrl} whenever it needs to generate a 
new Http Solr Client.
-     *
-     * @param solrClientBuilder A Builder like {@link Http2SolrClient.Builder} 
used to generate the
-     *     internal clients
-     * @param endpoints the initial Solr Endpoints to load balance
-     */
-    public Builder(B solrClientBuilder, Endpoint... endpoints) {
-      this.solrClientBuilder = solrClientBuilder;
+    public Builder(C solrClient, Endpoint... endpoints) {
+      this.solrClient = solrClient;
       this.solrEndpoints = endpoints;
     }
 
@@ -347,7 +309,7 @@ public class LBHttp2SolrClient<B extends 
HttpSolrClientBuilderBase<?, ?>> extend
      *
      * @param aliveCheckInterval how often to ping for aliveness
      */
-    public Builder<B> setAliveCheckInterval(int aliveCheckInterval, TimeUnit 
unit) {
+    public Builder<C> setAliveCheckInterval(int aliveCheckInterval, TimeUnit 
unit) {
       if (aliveCheckInterval <= 0) {
         throw new IllegalArgumentException(
             "Alive check interval must be " + "positive, specified value = " + 
aliveCheckInterval);
@@ -357,13 +319,13 @@ public class LBHttp2SolrClient<B extends 
HttpSolrClientBuilderBase<?, ?>> extend
     }
 
     /** Sets a default for core or collection based requests. */
-    public Builder<B> withDefaultCollection(String defaultCoreOrCollection) {
+    public Builder<C> withDefaultCollection(String defaultCoreOrCollection) {
       this.defaultCollection = defaultCoreOrCollection;
       return this;
     }
 
-    public LBHttp2SolrClient<B> build() {
-      return new LBHttp2SolrClient<B>(this);
+    public LBHttp2SolrClient<C> build() {
+      return new LBHttp2SolrClient<C>(this);
     }
   }
 }
diff --git 
a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/LBSolrClient.java 
b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/LBSolrClient.java
index 31c75662b48..64201b03c13 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/LBSolrClient.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/LBSolrClient.java
@@ -497,7 +497,25 @@ public abstract class LBSolrClient extends SolrClient {
   private NamedList<Object> doRequest(Endpoint endpoint, SolrRequest<?> 
solrRequest)
       throws SolrServerException, IOException {
     final var solrClient = getClient(endpoint);
-    return solrClient.request(solrRequest, endpoint.getCore());
+    return doRequest(solrClient, endpoint.getBaseUrl(), endpoint.getCore(), 
solrRequest);
+  }
+
+  // TODO SOLR-17541 should remove the need for the special-casing below; 
remove as a part of that
+  // ticket.
+  private NamedList<Object> doRequest(
+      SolrClient solrClient, String baseUrl, String collection, SolrRequest<?> 
solrRequest)
+      throws SolrServerException, IOException {
+    // Some implementations of LBSolrClient.getClient(...) return a 
Http2SolrClient that may not be
+    // pointed at the desired URL (or any URL for that matter).  We special 
case that here to ensure
+    // the appropriate URL is provided.
+    if (solrClient instanceof Http2SolrClient httpSolrClient) {
+      return httpSolrClient.requestWithBaseUrl(baseUrl, (c) -> 
c.request(solrRequest, collection));
+    } else if (solrClient instanceof HttpJdkSolrClient) {
+      return ((HttpJdkSolrClient) solrClient).requestWithBaseUrl(baseUrl, 
solrRequest, collection);
+    }
+
+    // Assume provided client already uses 'baseUrl'
+    return solrClient.request(solrRequest, collection);
   }
 
   protected Exception doRequest(
@@ -607,7 +625,12 @@ public abstract class LBSolrClient extends SolrClient {
       // First the one on the endpoint, then the default collection
       final String effectiveCollection =
           Objects.requireNonNullElse(zombieEndpoint.getCore(), 
getDefaultCollection());
-      final var responseRaw = getClient(zombieEndpoint).request(queryRequest, 
effectiveCollection);
+      final var responseRaw =
+          doRequest(
+              getClient(zombieEndpoint),
+              zombieEndpoint.getBaseUrl(),
+              effectiveCollection,
+              queryRequest);
       QueryResponse resp = new QueryResponse();
       resp.setResponse(responseRaw);
 
@@ -711,7 +734,7 @@ public abstract class LBSolrClient extends SolrClient {
         // Choose the endpoint's core/collection over any specified by the user
         final var effectiveCollection =
             endpoint.getCore() == null ? collection : endpoint.getCore();
-        return getClient(endpoint).request(request, effectiveCollection);
+        return doRequest(getClient(endpoint), endpoint.getBaseUrl(), 
effectiveCollection, request);
       } catch (SolrException e) {
         // Server is alive but the request was malformed or invalid
         throw e;
@@ -752,7 +775,8 @@ public abstract class LBSolrClient extends SolrClient {
         ++numServersTried;
         final String effectiveCollection =
             endpoint.getCore() == null ? collection : endpoint.getCore();
-        NamedList<Object> rsp = getClient(endpoint).request(request, 
effectiveCollection);
+        NamedList<Object> rsp =
+            doRequest(getClient(endpoint), endpoint.getBaseUrl(), 
effectiveCollection, request);
         // remove from zombie list *before* adding to the alive list to avoid 
a race that could lose
         // a server
         zombieServers.remove(endpoint.getUrl());
diff --git 
a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudHttp2SolrClientBuilderTest.java
 
b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudHttp2SolrClientBuilderTest.java
index 46b6883f755..0846dfefc6c 100644
--- 
a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudHttp2SolrClientBuilderTest.java
+++ 
b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/CloudHttp2SolrClientBuilderTest.java
@@ -120,6 +120,26 @@ public class CloudHttp2SolrClientBuilderTest extends 
SolrCloudTestCase {
     }
   }
 
+  @Test
+  public void testExternalClientAndInternalBuilderTogether() {
+    expectThrows(
+        IllegalStateException.class,
+        () ->
+            new CloudHttp2SolrClient.Builder(
+                    Collections.singletonList(ANY_ZK_HOST), 
Optional.of(ANY_CHROOT))
+                .withHttpClient(mock(Http2SolrClient.class))
+                .withInternalClientBuilder(mock(Http2SolrClient.Builder.class))
+                .build());
+    expectThrows(
+        IllegalStateException.class,
+        () ->
+            new CloudHttp2SolrClient.Builder(
+                    Collections.singletonList(ANY_ZK_HOST), 
Optional.of(ANY_CHROOT))
+                .withInternalClientBuilder(mock(Http2SolrClient.Builder.class))
+                .withHttpClient(mock(Http2SolrClient.class))
+                .build());
+  }
+
   @Test
   public void testProvideInternalBuilder() throws IOException {
     Http2SolrClient http2Client = mock(Http2SolrClient.class);
@@ -139,6 +159,20 @@ public class CloudHttp2SolrClientBuilderTest extends 
SolrCloudTestCase {
     verify(http2Client, times(1)).close();
   }
 
+  @Test
+  public void testProvideExternalClient() throws IOException {
+    Http2SolrClient http2Client = mock(Http2SolrClient.class);
+    CloudHttp2SolrClient.Builder clientBuilder =
+        new CloudHttp2SolrClient.Builder(
+                Collections.singletonList(ANY_ZK_HOST), 
Optional.of(ANY_CHROOT))
+            .withHttpClient(http2Client);
+    try (CloudHttp2SolrClient client = clientBuilder.build()) {
+      assertEquals(http2Client, client.getHttpClient());
+    }
+    // it's external, should be NOT closed when closing CloudSolrClient
+    verify(http2Client, never()).close();
+  }
+
   @Test
   public void testDefaultCollectionPassedFromBuilderToClient() throws 
IOException {
     try (CloudHttp2SolrClient createdClient =
@@ -161,19 +195,29 @@ public class CloudHttp2SolrClientBuilderTest extends 
SolrCloudTestCase {
   public void testHttpClientPreservedInHttp2ClusterStateProvider() throws 
IOException {
     List<String> solrUrls = 
List.of(cluster.getJettySolrRunner(0).getBaseUrl().toString());
 
-    // without internalClientBuilder
-    testHttpClientConsistency(solrUrls, null);
+    // No httpClient - No internalClientBuilder
+    testHttpClientConsistency(solrUrls, null, null);
+
+    // httpClient - No internalClientBuilder
+    try (Http2SolrClient httpClient = new Http2SolrClient.Builder().build()) {
+      testHttpClientConsistency(solrUrls, httpClient, null);
+    }
 
-    // with internalClientBuilder
+    // No httpClient - internalClientBuilder
     Http2SolrClient.Builder internalClientBuilder = new 
Http2SolrClient.Builder();
-    testHttpClientConsistency(solrUrls, internalClientBuilder);
+    testHttpClientConsistency(solrUrls, null, internalClientBuilder);
   }
 
   private void testHttpClientConsistency(
-      List<String> solrUrls, Http2SolrClient.Builder internalClientBuilder) 
throws IOException {
+      List<String> solrUrls,
+      Http2SolrClient httpClient,
+      Http2SolrClient.Builder internalClientBuilder)
+      throws IOException {
     CloudHttp2SolrClient.Builder clientBuilder = new 
CloudHttp2SolrClient.Builder(solrUrls);
 
-    if (internalClientBuilder != null) {
+    if (httpClient != null) {
+      clientBuilder.withHttpClient(httpClient);
+    } else if (internalClientBuilder != null) {
       clientBuilder.withInternalClientBuilder(internalClientBuilder);
     }
 
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 1dbfc0e998b..b3980ad44bc 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
@@ -34,6 +34,7 @@ import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrQuery;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.request.QueryRequest;
 import org.apache.solr.client.solrj.request.RequestWriter;
 import org.apache.solr.client.solrj.response.SolrPingResponse;
 import org.apache.solr.common.params.CommonParams;
@@ -153,6 +154,25 @@ public class HttpJdkSolrClientTest extends 
HttpSolrClientTestBase {
     }
   }
 
+  @Test
+  public void testRequestWithBaseUrl() throws Exception {
+    DebugServlet.clear();
+    DebugServlet.addResponseHeader("Content-Type", "application/octet-stream");
+    DebugServlet.responseBodyByQueryFragment.put("", javabinResponse());
+    String someOtherUrl = getBaseUrl() + "/some/other/base/url";
+    String intendedUrl = getBaseUrl() + DEBUG_SERVLET_PATH;
+    SolrQuery q = new SolrQuery("foo");
+    q.setParam("a", MUST_ENCODE);
+
+    HttpJdkSolrClient.Builder b =
+        builder(someOtherUrl).withResponseParser(new BinaryResponseParser());
+    try (HttpJdkSolrClient client = b.build()) {
+      client.requestWithBaseUrl(intendedUrl, new QueryRequest(q, 
SolrRequest.METHOD.GET), null);
+      assertEquals(
+          client.getParser().getVersion(), 
DebugServlet.parameters.get(CommonParams.VERSION)[0]);
+    }
+  }
+
   @Test
   public void testGetById() throws Exception {
     DebugServlet.clear();
diff --git 
a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/LBHttp2SolrClientIntegrationTest.java
 
b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/LBHttp2SolrClientIntegrationTest.java
index 9c2f79ff0a5..61504a052b8 100644
--- 
a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/LBHttp2SolrClientIntegrationTest.java
+++ 
b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/LBHttp2SolrClientIntegrationTest.java
@@ -18,6 +18,7 @@ package org.apache.solr.client.solrj.impl;
 
 import java.io.File;
 import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.lang.invoke.MethodHandles;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -117,28 +118,30 @@ public class LBHttp2SolrClientIntegrationTest extends 
SolrTestCaseJ4 {
 
   private LBClientHolder client(LBSolrClient.Endpoint... baseSolrEndpoints) {
     if (random().nextBoolean()) {
-      var delegateClientBuilder =
+      var delegateClient =
           new Http2SolrClient.Builder()
               .withConnectionTimeout(1000, TimeUnit.MILLISECONDS)
-              .withIdleTimeout(2000, TimeUnit.MILLISECONDS);
+              .withIdleTimeout(2000, TimeUnit.MILLISECONDS)
+              .build();
       var lbClient =
-          new LBHttp2SolrClient.Builder<>(delegateClientBuilder, 
baseSolrEndpoints)
+          new LBHttp2SolrClient.Builder<>(delegateClient, baseSolrEndpoints)
               .withDefaultCollection(solr[0].getDefaultCollection())
               .setAliveCheckInterval(500, TimeUnit.MILLISECONDS)
               .build();
-      return new LBClientHolder(lbClient, delegateClientBuilder);
+      return new LBClientHolder(lbClient, delegateClient);
     } else {
-      var delegateClientBuilder =
+      var delegateClient =
           new HttpJdkSolrClient.Builder()
               .withConnectionTimeout(1000, TimeUnit.MILLISECONDS)
               .withIdleTimeout(2000, TimeUnit.MILLISECONDS)
-              .withSSLContext(MockTrustManager.ALL_TRUSTING_SSL_CONTEXT);
+              .withSSLContext(MockTrustManager.ALL_TRUSTING_SSL_CONTEXT)
+              .build();
       var lbClient =
-          new LBHttp2SolrClient.Builder<>(delegateClientBuilder, 
baseSolrEndpoints)
+          new LBHttp2SolrClient.Builder<>(delegateClient, baseSolrEndpoints)
               .withDefaultCollection(solr[0].getDefaultCollection())
               .setAliveCheckInterval(500, TimeUnit.MILLISECONDS)
               .build();
-      return new LBClientHolder(lbClient, delegateClientBuilder);
+      return new LBClientHolder(lbClient, delegateClient);
     }
   }
 
@@ -314,9 +317,9 @@ public class LBHttp2SolrClientIntegrationTest extends 
SolrTestCaseJ4 {
   private static class LBClientHolder implements AutoCloseable {
 
     final LBHttp2SolrClient<?> lbClient;
-    final HttpSolrClientBuilderBase<?, ?> delegate;
+    final HttpSolrClientBase delegate;
 
-    LBClientHolder(LBHttp2SolrClient<?> lbClient, HttpSolrClientBuilderBase<?, 
?> delegate) {
+    LBClientHolder(LBHttp2SolrClient<?> lbClient, HttpSolrClientBase delegate) 
{
       this.lbClient = lbClient;
       this.delegate = delegate;
     }
@@ -324,6 +327,11 @@ public class LBHttp2SolrClientIntegrationTest extends 
SolrTestCaseJ4 {
     @Override
     public void close() {
       lbClient.close();
+      try {
+        delegate.close();
+      } catch (IOException ioe) {
+        throw new UncheckedIOException(ioe);
+      }
     }
   }
 }
diff --git 
a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/LBHttp2SolrClientTest.java
 
b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/LBHttp2SolrClientTest.java
index 30cee0804b5..9d2019309b0 100644
--- 
a/solr/solrj/src/test/org/apache/solr/client/solrj/impl/LBHttp2SolrClientTest.java
+++ 
b/solr/solrj/src/test/org/apache/solr/client/solrj/impl/LBHttp2SolrClientTest.java
@@ -18,8 +18,6 @@ package org.apache.solr.client.solrj.impl;
 
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
@@ -30,6 +28,7 @@ import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import org.apache.solr.SolrTestCase;
+import org.apache.solr.client.solrj.SolrClientFunction;
 import org.apache.solr.client.solrj.SolrRequest;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.request.QueryRequest;
@@ -52,12 +51,11 @@ public class LBHttp2SolrClientTest extends SolrTestCase {
     Set<String> urlParamNames = new HashSet<>(2);
     urlParamNames.add("param1");
 
-    var httpSolrClientBuilder =
-        new 
Http2SolrClient.Builder(url).withTheseParamNamesInTheUrl(urlParamNames);
-    var endpoint = new LBSolrClient.Endpoint(url);
-    try (var testClient =
-        new 
LBHttp2SolrClient.Builder<Http2SolrClient.Builder>(httpSolrClientBuilder, 
endpoint)
-            .build()) {
+    try (Http2SolrClient http2SolrClient =
+            new 
Http2SolrClient.Builder(url).withTheseParamNamesInTheUrl(urlParamNames).build();
+        LBHttp2SolrClient<Http2SolrClient> testClient =
+            new LBHttp2SolrClient.Builder<>(http2SolrClient, new 
LBSolrClient.Endpoint(url))
+                .build()) {
 
       assertArrayEquals(
           "Wrong urlParamNames found in lb client.",
@@ -66,7 +64,7 @@ public class LBHttp2SolrClientTest extends SolrTestCase {
       assertArrayEquals(
           "Wrong urlParamNames found in base client.",
           urlParamNames.toArray(),
-          testClient.getClient(endpoint).getUrlParamNames().toArray());
+          http2SolrClient.getUrlParamNames().toArray());
     }
   }
 
@@ -76,11 +74,12 @@ public class LBHttp2SolrClientTest extends SolrTestCase {
     LBSolrClient.Endpoint ep2 = new 
LBSolrClient.Endpoint("http://endpoint.two";);
     List<LBSolrClient.Endpoint> endpointList = List.of(ep1, ep2);
 
-    var httpSolrClientBuilder =
-        new MockHttpSolrClientBuilder().withConnectionTimeout(10, 
TimeUnit.SECONDS);
-
-    try (LBHttp2SolrClient<MockHttpSolrClientBuilder> testClient =
-        new LBHttp2SolrClient.Builder<>(httpSolrClientBuilder, ep1, 
ep2).build()) {
+    Http2SolrClient.Builder b =
+        new 
Http2SolrClient.Builder("http://base.url";).withConnectionTimeout(10, 
TimeUnit.SECONDS);
+    ;
+    try (MockHttpSolrClient client = new MockHttpSolrClient("http://base.url";, 
b);
+        LBHttp2SolrClient<MockHttpSolrClient> testClient =
+            new LBHttp2SolrClient.Builder<>(client, ep1, ep2).build()) {
 
       String lastEndpoint = null;
       for (int i = 0; i < 10; i++) {
@@ -104,14 +103,15 @@ public class LBHttp2SolrClientTest extends SolrTestCase {
     LBSolrClient.Endpoint ep2 = new 
LBSolrClient.Endpoint("http://endpoint.two";);
     List<LBSolrClient.Endpoint> endpointList = List.of(ep1, ep2);
 
-    var httpSolrClientBuilder =
-        new MockHttpSolrClientBuilder().withConnectionTimeout(10, 
TimeUnit.SECONDS);
-
-    try (LBHttp2SolrClient<MockHttpSolrClientBuilder> testClient =
-        new LBHttp2SolrClient.Builder<>(httpSolrClientBuilder, ep1, 
ep2).build()) {
+    Http2SolrClient.Builder b =
+        new 
Http2SolrClient.Builder("http://base.url";).withConnectionTimeout(10, 
TimeUnit.SECONDS);
+    ;
+    try (MockHttpSolrClient client = new MockHttpSolrClient("http://base.url";, 
b);
+        LBHttp2SolrClient<MockHttpSolrClient> testClient =
+            new LBHttp2SolrClient.Builder<>(client, ep1, ep2).build()) {
 
-      setEndpointToFail(testClient, ep1);
-      setEndpointToSucceed(testClient, ep2);
+      client.basePathToFail = ep1.getBaseUrl();
+      String basePathToSucceed = ep2.getBaseUrl();
       String qValue = "First time";
 
       for (int i = 0; i < 5; i++) {
@@ -121,13 +121,13 @@ public class LBHttp2SolrClientTest extends SolrTestCase {
         LBSolrClient.Rsp response = testClient.request(req);
         assertEquals(
             "The healthy node 'endpoint two' should have served the request: " 
+ i,
-            ep2.getBaseUrl(),
+            basePathToSucceed,
             response.server);
         checkSynchonousResponseContent(response, qValue);
       }
 
-      setEndpointToFail(testClient, ep2);
-      setEndpointToSucceed(testClient, ep1);
+      client.basePathToFail = ep2.getBaseUrl();
+      basePathToSucceed = ep1.getBaseUrl();
       qValue = "Second time";
 
       for (int i = 0; i < 5; i++) {
@@ -137,13 +137,21 @@ public class LBHttp2SolrClientTest extends SolrTestCase {
         LBSolrClient.Rsp response = testClient.request(req);
         assertEquals(
             "The healthy node 'endpoint one' should have served the request: " 
+ i,
-            ep1.getBaseUrl(),
+            basePathToSucceed,
             response.server);
         checkSynchonousResponseContent(response, qValue);
       }
     }
   }
 
+  private void checkSynchonousResponseContent(LBSolrClient.Rsp response, 
String qValue) {
+    assertEquals("There should be one element in the respnse.", 1, 
response.getResponse().size());
+    assertEquals(
+        "The response key 'response' should echo the query.",
+        qValue,
+        response.getResponse().get("response"));
+  }
+
   @Test
   public void testAsyncWithFailures() {
 
@@ -154,35 +162,28 @@ public class LBHttp2SolrClientTest extends SolrTestCase {
     LBSolrClient.Endpoint ep2 = new 
LBSolrClient.Endpoint("http://endpoint.two";);
     List<LBSolrClient.Endpoint> endpointList = List.of(ep1, ep2);
 
-    var httpSolrClientBuilder =
-        new MockHttpSolrClientBuilder().withConnectionTimeout(10, 
TimeUnit.SECONDS);
-
-    try (LBHttp2SolrClient<MockHttpSolrClientBuilder> testClient =
-        new LBHttp2SolrClient.Builder<>(httpSolrClientBuilder, ep1, 
ep2).build()) {
+    Http2SolrClient.Builder b =
+        new 
Http2SolrClient.Builder("http://base.url";).withConnectionTimeout(10, 
TimeUnit.SECONDS);
+    ;
+    try (MockHttpSolrClient client = new MockHttpSolrClient("http://base.url";, 
b);
+        LBHttp2SolrClient<MockHttpSolrClient> testClient =
+            new LBHttp2SolrClient.Builder<>(client, ep1, ep2).build()) {
 
       for (int j = 0; j < 2; j++) {
         // first time Endpoint One will return error code 500.
         // second time Endpoint One will be healthy
 
-        LBSolrClient.Endpoint endpointToSucceed;
-        LBSolrClient.Endpoint endpointToFail;
+        String basePathToSucceed;
         if (j == 0) {
-          setEndpointToFail(testClient, ep1);
-          setEndpointToSucceed(testClient, ep2);
-          endpointToSucceed = ep2;
-          endpointToFail = ep1;
+          client.basePathToFail = ep1.getBaseUrl();
+          basePathToSucceed = ep2.getBaseUrl();
         } else {
-          setEndpointToFail(testClient, ep2);
-          setEndpointToSucceed(testClient, ep1);
-          endpointToSucceed = ep1;
-          endpointToFail = ep2;
+          client.basePathToFail = ep2.getBaseUrl();
+          basePathToSucceed = ep1.getBaseUrl();
         }
-        List<String> successEndpointLastBasePaths =
-            basePathsForEndpoint(testClient, endpointToSucceed);
-        List<String> failEndpointLastBasePaths = 
basePathsForEndpoint(testClient, endpointToFail);
 
         for (int i = 0; i < 10; i++) {
-          // i: we'll try 10 times.  It should behave the same with iter 2-10. 
.
+          // i: we'll try 10 times to see if it behaves the same every time.
 
           QueryRequest queryRequest = new QueryRequest(new 
MapSolrParams(Map.of("q", "" + i)));
           LBSolrClient.Req req = new LBSolrClient.Req(queryRequest, 
endpointList);
@@ -195,26 +196,26 @@ public class LBHttp2SolrClientTest extends SolrTestCase {
           } catch (TimeoutException | ExecutionException e) {
             fail(iterMessage + " Response ended in failure: " + e);
           }
-
           if (i == 0) {
-            // When i=0, it must try both endpoints to find success:
+            // When j=0, "endpoint one" fails.
+            // The first time around (i) it tries the first, then the second.
+            //
+            // With j=0 and i>0, it only tries "endpoint two".
             //
-            //     with j=0, endpoint one is tried first because it
-            //               is first one the list, but it fails.
-            //     with j=1, endpoint two is tried first because
-            //               it is the only known healthy node, but
-            //               now it is failing.
-            assertEquals(iterMessage, 1, successEndpointLastBasePaths.size());
-            assertEquals(iterMessage, 1, failEndpointLastBasePaths.size());
+            // When j=1 and i=0, "endpoint two" starts failing.
+            // So it tries both it and "endpoint one"
+            //
+            // With j=1 and i>0, it only tries "endpoint one".
+            assertEquals(iterMessage, 2, client.lastBasePaths.size());
+
+            String failedBasePath = client.lastBasePaths.remove(0);
+            assertEquals(iterMessage, client.basePathToFail, failedBasePath);
           } else {
-            // With i>0,
-            //     With j=0 and i>0, it only tries "endpoint two".
-            //     With j=1 and i>0, it only tries "endpoint one".
-            assertEquals(iterMessage, 1, successEndpointLastBasePaths.size());
-            assertTrue(iterMessage, failEndpointLastBasePaths.isEmpty());
+            // The first endpoint does not give the exception, it doesn't 
retry.
+            assertEquals(iterMessage, 1, client.lastBasePaths.size());
           }
-          successEndpointLastBasePaths.clear();
-          failEndpointLastBasePaths.clear();
+          String successBasePath = client.lastBasePaths.remove(0);
+          assertEquals(iterMessage, basePathToSucceed, successBasePath);
         }
       }
     }
@@ -226,11 +227,11 @@ public class LBHttp2SolrClientTest extends SolrTestCase {
     LBSolrClient.Endpoint ep2 = new 
LBSolrClient.Endpoint("http://endpoint.two";);
     List<LBSolrClient.Endpoint> endpointList = List.of(ep1, ep2);
 
-    var httpSolrClientBuilder =
-        new MockHttpSolrClientBuilder().withConnectionTimeout(10, 
TimeUnit.SECONDS);
-
-    try (LBHttp2SolrClient<MockHttpSolrClientBuilder> testClient =
-        new LBHttp2SolrClient.Builder<>(httpSolrClientBuilder, ep1, 
ep2).build()) {
+    Http2SolrClient.Builder b =
+        new 
Http2SolrClient.Builder("http://base.url";).withConnectionTimeout(10, 
TimeUnit.SECONDS);
+    try (MockHttpSolrClient client = new MockHttpSolrClient("http://base.url";, 
b);
+        LBHttp2SolrClient<MockHttpSolrClient> testClient =
+            new LBHttp2SolrClient.Builder<>(client, ep1, ep2).build()) {
 
       int limit = 10; // For simplicity use an even limit
       List<CompletableFuture<LBSolrClient.Rsp>> responses = new ArrayList<>();
@@ -242,17 +243,23 @@ public class LBHttp2SolrClientTest extends SolrTestCase {
       }
 
       QueryRequest[] queryRequests = new QueryRequest[limit];
-      List<SolrRequest<?>> lastSolrRequests = lastSolrRequests(testClient, 
ep1, ep2);
-      assertEquals(limit, lastSolrRequests.size());
-
+      int numEndpointOne = 0;
+      int numEndpointTwo = 0;
       for (int i = 0; i < limit; i++) {
-        SolrRequest<?> lastSolrReq = lastSolrRequests.get(i);
+        SolrRequest<?> lastSolrReq = client.lastSolrRequests.get(i);
         assertTrue(lastSolrReq instanceof QueryRequest);
         QueryRequest lastQueryReq = (QueryRequest) lastSolrReq;
         int index = Integer.parseInt(lastQueryReq.getParams().get("q"));
         assertNull("Found same request twice: " + index, queryRequests[index]);
         queryRequests[index] = lastQueryReq;
 
+        final String lastBasePath = client.lastBasePaths.get(i);
+        if (lastBasePath.equals(ep1.toString())) {
+          numEndpointOne++;
+        } else if (lastBasePath.equals(ep2.toString())) {
+          numEndpointTwo++;
+        }
+
         LBSolrClient.Rsp lastRsp = null;
         try {
           lastRsp = responses.get(index).get();
@@ -271,55 +278,15 @@ public class LBHttp2SolrClientTest extends SolrTestCase {
       // It is the user's responsibility to shuffle the endpoints when using
       // async.  LB Http Solr Client will always try the passed-in endpoints
       // in order.  In this case, endpoint 1 gets all the requests!
-      List<String> ep1BasePaths = basePathsForEndpoint(testClient, ep1);
-      List<String> ep2BasePaths = basePathsForEndpoint(testClient, ep2);
-      assertEquals(limit, basePathsForEndpoint(testClient, ep1).size());
-      assertEquals(0, basePathsForEndpoint(testClient, ep2).size());
-    }
-  }
-
-  private void checkSynchonousResponseContent(LBSolrClient.Rsp response, 
String qValue) {
-    assertEquals("There should be one element in the response.", 1, 
response.getResponse().size());
-    assertEquals(
-        "The response key 'response' should echo the query.",
-        qValue,
-        response.getResponse().get("response"));
-  }
-
-  private void setEndpointToFail(
-      LBHttp2SolrClient<MockHttpSolrClientBuilder> testClient, 
LBSolrClient.Endpoint ep) {
-    ((MockHttpSolrClient) testClient.getClient(ep)).allRequestsShallFail = 
true;
-  }
+      assertEquals(limit, numEndpointOne);
+      assertEquals(0, numEndpointTwo);
 
-  private void setEndpointToSucceed(
-      LBHttp2SolrClient<MockHttpSolrClientBuilder> testClient, 
LBSolrClient.Endpoint ep) {
-    ((MockHttpSolrClient) testClient.getClient(ep)).allRequestsShallFail = 
false;
-  }
-
-  private List<String> basePathsForEndpoint(
-      LBHttp2SolrClient<MockHttpSolrClientBuilder> testClient, 
LBSolrClient.Endpoint ep) {
-    return ((MockHttpSolrClient) testClient.getClient(ep)).lastBasePaths;
-  }
-
-  private List<SolrRequest<?>> lastSolrRequests(
-      LBHttp2SolrClient<MockHttpSolrClientBuilder> testClient, 
LBSolrClient.Endpoint... endpoints) {
-    return Arrays.stream(endpoints)
-        .map(testClient::getClient)
-        .map(MockHttpSolrClient.class::cast)
-        .flatMap(c -> c.lastSolrRequests.stream())
-        .toList();
-  }
-
-  public static class MockHttpSolrClientBuilder
-      extends HttpSolrClientBuilderBase<MockHttpSolrClientBuilder, 
MockHttpSolrClient> {
-
-    @Override
-    public MockHttpSolrClient build() {
-      return new MockHttpSolrClient(baseSolrUrl, this);
+      assertEquals(limit, client.lastSolrRequests.size());
+      assertEquals(limit, client.lastCollections.size());
     }
   }
 
-  public static class MockHttpSolrClient extends HttpSolrClientBase {
+  public static class MockHttpSolrClient extends Http2SolrClient {
 
     public List<SolrRequest<?>> lastSolrRequests = new ArrayList<>();
 
@@ -327,13 +294,15 @@ public class LBHttp2SolrClientTest extends SolrTestCase {
 
     public List<String> lastCollections = new ArrayList<>();
 
-    public boolean allRequestsShallFail;
+    public String basePathToFail = null;
 
     public String tmpBaseUrl = null;
 
-    public boolean closeCalled;
+    protected MockHttpSolrClient(String serverBaseUrl, Builder builder) {
 
-    protected MockHttpSolrClient(String serverBaseUrl, 
MockHttpSolrClientBuilder builder) {
+      // TODO: Consider creating an interface for Http*SolrClient
+      // so mocks can Implement, not Extend, and not actually need to
+      // build an (unused) client
       super(serverBaseUrl, builder);
     }
 
@@ -343,12 +312,25 @@ public class LBHttp2SolrClientTest extends SolrTestCase {
       lastSolrRequests.add(request);
       lastBasePaths.add(tmpBaseUrl);
       lastCollections.add(collection);
-      if (allRequestsShallFail) {
+      if (tmpBaseUrl.equals(basePathToFail)) {
         throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "We 
should retry this.");
       }
       return generateResponse(request);
     }
 
+    @Override
+    public <R> R requestWithBaseUrl(
+        String baseUrl, SolrClientFunction<Http2SolrClient, R> clientFunction)
+        throws SolrServerException, IOException {
+      // This use of 'tmpBaseUrl' is NOT thread safe, but that's fine for our 
purposes here.
+      try {
+        tmpBaseUrl = baseUrl;
+        return clientFunction.apply(this);
+      } finally {
+        tmpBaseUrl = null;
+      }
+    }
+
     @Override
     public CompletableFuture<NamedList<Object>> requestAsync(
         final SolrRequest<?> solrRequest, String collection) {
@@ -356,7 +338,7 @@ public class LBHttp2SolrClientTest extends SolrTestCase {
       lastSolrRequests.add(solrRequest);
       lastBasePaths.add(tmpBaseUrl);
       lastCollections.add(collection);
-      if (allRequestsShallFail) {
+      if (tmpBaseUrl != null && tmpBaseUrl.equals(basePathToFail)) {
         cf.completeExceptionally(
             new SolrException(SolrException.ErrorCode.SERVER_ERROR, "We should 
retry this."));
       } else {
@@ -369,32 +351,5 @@ public class LBHttp2SolrClientTest extends SolrTestCase {
       String id = solrRequest.getParams().get("q");
       return new NamedList<>(Collections.singletonMap("response", id));
     }
-
-    @Override
-    public void close() throws IOException {
-      closeCalled = true;
-    }
-
-    @Override
-    protected boolean isFollowRedirects() {
-      return false;
-    }
-
-    @Override
-    protected boolean processorAcceptsMimeType(
-        Collection<String> processorSupportedContentTypes, String mimeType) {
-      return false;
-    }
-
-    @Override
-    protected String allProcessorSupportedContentTypesCommaDelimited(
-        Collection<String> processorSupportedContentTypes) {
-      return null;
-    }
-
-    @Override
-    protected void updateDefaultMimeTypeForParser() {
-      // no-op
-    }
   }
 }

Reply via email to