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 d3e57aad825 SOLR-17541: LBSolrClient implementations should agree on 
'getClient()' semantics (#2899)
d3e57aad825 is described below

commit d3e57aad8259f4b9c574fbff5cb5f6d112bef85a
Author: James Dyer <jd...@apache.org>
AuthorDate: Mon Dec 23 09:04:33 2024 -0600

    SOLR-17541: LBSolrClient implementations should agree on 'getClient()' 
semantics (#2899)
---
 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, 344 insertions(+), 348 deletions(-)

diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index a844220f2fc..a135ecaa539 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -34,6 +34,10 @@ 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.
@@ -102,6 +106,11 @@ 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)
@@ -150,7 +159,10 @@ New Features
 
 Improvements
 ---------------------
-(No changes)
+* SOLR-17541: Deprecate `CloudHttp2SolrClient.Builder#withHttpClient` in favor 
of
+  `CloudHttp2SolrClient.Builder#withInternalClientBuilder`.
+   Deprecate `LBHttp2SolrClient.Builder#withListenerFactory` in favor of
+   `LBHttp2SolrClient.Builder#withListenerFactories` (James Dyer)
 
 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 7dd30a14d24..5a890121a43 100644
--- a/solr/core/src/java/org/apache/solr/cloud/ZkController.java
+++ b/solr/core/src/java/org/apache/solr/cloud/ZkController.java
@@ -197,9 +197,6 @@ 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
@@ -754,7 +751,6 @@ public class ZkController implements Closeable {
       sysPropsCacher.close();
       customThreadPool.execute(() -> IOUtils.closeQuietly(cloudManager));
       customThreadPool.execute(() -> IOUtils.closeQuietly(cloudSolrClient));
-      customThreadPool.execute(() -> IOUtils.closeQuietly(http2SolrClient));
 
       try {
         try {
@@ -850,15 +846,14 @@ public class ZkController implements Closeable {
       if (cloudManager != null) {
         return cloudManager;
       }
-      http2SolrClient =
+      var httpSolrClientBuilder =
           new Http2SolrClient.Builder()
               .withHttpClient(cc.getDefaultHttpSolrClient())
               .withIdleTimeout(30000, TimeUnit.MILLISECONDS)
-              .withConnectionTimeout(15000, TimeUnit.MILLISECONDS)
-              .build();
+              .withConnectionTimeout(15000, TimeUnit.MILLISECONDS);
       cloudSolrClient =
           new CloudHttp2SolrClient.Builder(new 
ZkClientClusterStateProvider(zkStateReader))
-              .withHttpClient(http2SolrClient)
+              .withInternalClientBuilder(httpSolrClientBuilder)
               .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 2bf25a896f6..e9631b26d1f 100644
--- a/solr/core/src/java/org/apache/solr/core/HttpSolrClientProvider.java
+++ b/solr/core/src/java/org/apache/solr/core/HttpSolrClientProvider.java
@@ -16,7 +16,6 @@
  */
 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;
@@ -37,22 +36,24 @@ 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);
 
-    Http2SolrClient.Builder httpClientBuilder =
-        new 
Http2SolrClient.Builder().withListenerFactory(List.of(trackHttpSolrMetrics));
+    this.httpSolrClientBuilder =
+        new Http2SolrClient.Builder().addListenerFactory(trackHttpSolrMetrics);
 
     if (cfg != null) {
-      httpClientBuilder
+      httpSolrClientBuilder
           .withConnectionTimeout(cfg.getDistributedConnectionTimeout(), 
TimeUnit.MILLISECONDS)
           .withIdleTimeout(cfg.getDistributedSocketTimeout(), 
TimeUnit.MILLISECONDS)
           .withMaxConnectionsPerHost(cfg.getMaxUpdateConnectionsPerHost());
     }
-    httpSolrClient = httpClientBuilder.build();
+    httpSolrClient = httpSolrClientBuilder.build();
   }
 
   private InstrumentedHttpListenerFactory.NameStrategy getNameStrategy(
@@ -76,7 +77,7 @@ final class HttpSolrClientProvider implements AutoCloseable {
   }
 
   void setSecurityBuilder(HttpClientBuilderPlugin builder) {
-    builder.setup(httpSolrClient);
+    builder.setup(httpSolrClientBuilder, 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 7592eed86fc..320a5fe70d7 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> lbClient;
+  protected LBHttp2SolrClient<Http2SolrClient.Builder> 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 ac7dc0cf8e0..1437dee63ea 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,8 +83,9 @@ public class HttpShardHandlerFactory extends 
ShardHandlerFactory
   protected ExecutorService commExecutor;
 
   protected volatile Http2SolrClient defaultClient;
+  protected Http2SolrClient.Builder httpSolrClientBuilder;
   protected InstrumentedHttpListenerFactory httpListenerFactory;
-  protected LBHttp2SolrClient<Http2SolrClient> loadbalancer;
+  protected LBHttp2SolrClient<Http2SolrClient.Builder> loadbalancer;
 
   int corePoolSize = 0;
   int maximumPoolSize = Integer.MAX_VALUE;
@@ -305,16 +306,16 @@ public class HttpShardHandlerFactory extends 
ShardHandlerFactory
             sb);
     int soTimeout =
         getParameter(args, HttpClientUtil.PROP_SO_TIMEOUT, 
HttpClientUtil.DEFAULT_SO_TIMEOUT, sb);
-
-    this.defaultClient =
+    this.httpSolrClientBuilder =
         new Http2SolrClient.Builder()
             .withConnectionTimeout(connectionTimeout, TimeUnit.MILLISECONDS)
             .withIdleTimeout(soTimeout, TimeUnit.MILLISECONDS)
             .withExecutor(commExecutor)
             .withMaxConnectionsPerHost(maxConnectionsPerHost)
-            .build();
-    this.defaultClient.addListenerFactory(this.httpListenerFactory);
-    this.loadbalancer = new 
LBHttp2SolrClient.Builder<Http2SolrClient>(defaultClient).build();
+            .addListenerFactory(this.httpListenerFactory);
+    this.defaultClient = httpSolrClientBuilder.build();
+
+    this.loadbalancer = new 
LBHttp2SolrClient.Builder<>(httpSolrClientBuilder).build();
 
     initReplicaListTransformers(getParameter(args, "replicaRouting", null, 
sb));
 
@@ -324,7 +325,7 @@ public class HttpShardHandlerFactory extends 
ShardHandlerFactory
   @Override
   public void setSecurityBuilder(HttpClientBuilderPlugin clientBuilderPlugin) {
     if (clientBuilderPlugin != null) {
-      clientBuilderPlugin.setup(defaultClient);
+      clientBuilderPlugin.setup(httpSolrClientBuilder, 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 b43d5f22190..206fb3d0886 100644
--- a/solr/core/src/java/org/apache/solr/security/HttpClientBuilderPlugin.java
+++ b/solr/core/src/java/org/apache/solr/security/HttpClientBuilderPlugin.java
@@ -34,4 +34,9 @@ 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 b1f6e6b6eed..93ecdcc9d68 100644
--- a/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java
+++ b/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java
@@ -376,6 +376,11 @@ 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";
@@ -431,7 +436,12 @@ public class PKIAuthenticationPlugin extends 
AuthenticationPlugin
                 (String) request.getAttributes().get(CACHED_REQUEST_USER_KEY));
           }
         };
-    client.addListenerFactory(() -> listener);
+    if (client != null) {
+      client.addListenerFactory(() -> listener);
+    }
+    if (builder != null) {
+      builder.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 758ae9ccd51..9eb3e783a2d 100644
--- a/solr/core/src/test/org/apache/solr/cli/PostToolTest.java
+++ b/solr/core/src/test/org/apache/solr/cli/PostToolTest.java
@@ -76,6 +76,7 @@ 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 bad8d58d021..e611902898f 100644
--- a/solr/core/src/test/org/apache/solr/cloud/OverseerTest.java
+++ b/solr/core/src/test/org/apache/solr/cloud/OverseerTest.java
@@ -1945,17 +1945,16 @@ public class OverseerTest extends SolrTestCaseJ4 {
   }
 
   private SolrCloudManager getCloudDataProvider(ZkStateReader zkStateReader) {
-    var httpSolrClient =
+    var httpSolrClientBuilder =
         new Http2SolrClient.Builder()
             .withIdleTimeout(30000, TimeUnit.MILLISECONDS)
-            .withConnectionTimeout(15000, TimeUnit.MILLISECONDS)
-            .build();
+            .withConnectionTimeout(15000, TimeUnit.MILLISECONDS);
     var cloudSolrClient =
         new CloudHttp2SolrClient.Builder(new 
ZkClientClusterStateProvider(zkStateReader))
-            .withHttpClient(httpSolrClient)
+            .withInternalClientBuilder(httpSolrClientBuilder)
             .build();
     solrClients.add(cloudSolrClient);
-    solrClients.add(httpSolrClient);
+    solrClients.add(httpSolrClientBuilder.build());
     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 0adb49471dc..68246001a40 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,7 +18,11 @@ package org.apache.solr.client.solrj;
 
 import java.io.IOException;
 
-/** A lambda intended for invoking SolrClient operations */
+/**
+ * A lambda intended for invoking SolrClient operations
+ *
+ * @lucene.experimental
+ */
 @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 ade1ebe433f..18aa318f8ba 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,9 +40,8 @@ import org.apache.solr.common.SolrException;
 public class CloudHttp2SolrClient extends CloudSolrClient {
 
   private final ClusterStateProvider stateProvider;
-  private final LBHttp2SolrClient<Http2SolrClient> lbClient;
+  private final LBHttp2SolrClient<Http2SolrClient.Builder> 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
@@ -54,8 +53,8 @@ public class CloudHttp2SolrClient extends CloudSolrClient {
    */
   protected CloudHttp2SolrClient(Builder builder) {
     super(builder.shardLeadersOnly, builder.parallelUpdates, 
builder.directUpdatesToLeadersOnly);
-    this.clientIsInternal = builder.httpClient == null;
-    this.myClient = createOrGetHttpClientFromBuilder(builder);
+    var httpSolrClientBuilder = createOrGetHttpClientBuilder(builder);
+    this.myClient = httpSolrClientBuilder.build();
     this.stateProvider = createClusterStateProvider(builder);
     this.retryExpiryTimeNano = builder.retryExpiryTimeNano;
     this.defaultCollection = builder.defaultCollection;
@@ -73,16 +72,14 @@ public class CloudHttp2SolrClient extends CloudSolrClient {
     // locks.
     this.locks = objectList(builder.parallelCacheRefreshesLocks);
 
-    this.lbClient = new 
LBHttp2SolrClient.Builder<Http2SolrClient>(myClient).build();
+    this.lbClient = new 
LBHttp2SolrClient.Builder<>(httpSolrClientBuilder).build();
   }
 
-  private Http2SolrClient createOrGetHttpClientFromBuilder(Builder builder) {
-    if (builder.httpClient != null) {
-      return builder.httpClient;
-    } else if (builder.internalClientBuilder != null) {
-      return builder.internalClientBuilder.build();
+  private Http2SolrClient.Builder createOrGetHttpClientBuilder(Builder 
builder) {
+    if (builder.internalClientBuilder != null) {
+      return builder.internalClientBuilder;
     } else {
-      return new Http2SolrClient.Builder().build();
+      return new Http2SolrClient.Builder();
     }
   }
 
@@ -129,7 +126,7 @@ public class CloudHttp2SolrClient extends CloudSolrClient {
 
   private void closeMyClientIfNeeded() {
     try {
-      if (clientIsInternal && myClient != null) {
+      if (myClient != null) {
         myClient.close();
       }
     } catch (Exception e) {
@@ -148,7 +145,7 @@ public class CloudHttp2SolrClient extends CloudSolrClient {
   }
 
   @Override
-  public LBHttp2SolrClient<Http2SolrClient> getLbClient() {
+  public LBHttp2SolrClient<Http2SolrClient.Builder> getLbClient() {
     return lbClient;
   }
 
@@ -171,7 +168,6 @@ 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;
@@ -404,23 +400,6 @@ 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
@@ -430,10 +409,6 @@ 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 fb2eb1a123f..fddc4e2daa6 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,9 +140,7 @@ public class Http2SolrClient extends HttpSolrClientBase {
       this.httpClient = createHttpClient(builder);
       this.closeClient = true;
     }
-    if (builder.listenerFactory != null) {
-      this.listenerFactory.addAll(builder.listenerFactory);
-    }
+    this.listenerFactory.addAll(builder.listenerFactories);
     updateDefaultMimeTypeForParser();
 
     
this.httpClient.setFollowRedirects(Boolean.TRUE.equals(builder.followRedirects));
@@ -571,6 +569,7 @@ 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)
@@ -906,7 +905,7 @@ public class Http2SolrClient extends HttpSolrClientBase {
 
     protected Long keyStoreReloadIntervalSecs;
 
-    private List<HttpListenerFactory> listenerFactory;
+    private List<HttpListenerFactory> listenerFactories = new ArrayList<>(0);
 
     public Builder() {
       super();
@@ -932,8 +931,27 @@ public class Http2SolrClient extends HttpSolrClientBase {
       this.baseSolrUrl = baseSolrUrl;
     }
 
-    public Http2SolrClient.Builder 
withListenerFactory(List<HttpListenerFactory> listenerFactory) {
-      this.listenerFactory = listenerFactory;
+    /**
+     * 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);
       return this;
     }
 
@@ -1109,9 +1127,9 @@ public class Http2SolrClient extends HttpSolrClientBase {
       if (this.urlParamNames == null) {
         this.urlParamNames = http2SolrClient.urlParamNames;
       }
-      if (this.listenerFactory == null) {
-        this.listenerFactory = new ArrayList<HttpListenerFactory>();
-        http2SolrClient.listenerFactory.forEach(this.listenerFactory::add);
+      if (this.listenerFactories.isEmpty()) {
+        this.listenerFactories.clear();
+        http2SolrClient.listenerFactory.forEach(this.listenerFactories::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 1127b3fd1a1..c26d13606b6 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, null);
+      PreparedRequest pReq = prepareRequest(solrRequest, collection);
       return httpClient
           .sendAsync(pReq.reqb.build(), 
HttpResponse.BodyHandlers.ofInputStream())
           .thenApply(
@@ -157,10 +157,10 @@ public class HttpJdkSolrClient extends HttpSolrClientBase 
{
     }
   }
 
-  protected NamedList<Object> requestWithBaseUrl(
-      String baseUrl, SolrRequest<?> solrRequest, String collection)
+  @Override
+  public NamedList<Object> request(SolrRequest<?> solrRequest, String 
collection)
       throws SolrServerException, IOException {
-    PreparedRequest pReq = prepareRequest(solrRequest, collection, baseUrl);
+    PreparedRequest pReq = prepareRequest(solrRequest, collection);
     HttpResponse<InputStream> response = null;
     try {
       response = httpClient.send(pReq.reqb.build(), 
HttpResponse.BodyHandlers.ofInputStream());
@@ -192,25 +192,13 @@ public class HttpJdkSolrClient extends HttpSolrClientBase 
{
     }
   }
 
-  @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)
+  private PreparedRequest prepareRequest(SolrRequest<?> solrRequest, String 
collection)
       throws SolrServerException, IOException {
     checkClosed();
     if (ClientUtils.shouldApplyDefaultCollection(collection, solrRequest)) {
       collection = defaultCollection;
     }
-    String url;
-    if (overrideBaseUrl != null) {
-      url = ClientUtils.buildRequestUrl(solrRequest, overrideBaseUrl, 
collection);
-    } else {
-      url = getRequestUrl(solrRequest, collection);
-    }
+    String 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 2c926a26261..4c0d46b13db 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,22 +23,25 @@ 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 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.
+ * 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.
  *
  * <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.
@@ -56,7 +59,7 @@ import org.slf4j.MDC;
  * <blockquote>
  *
  * <pre>
- * SolrClient client = new LBHttp2SolrClient.Builder(http2SolrClient,
+ * SolrClient client = new LBHttp2SolrClient.Builder(http2SolrClientBuilder,
  *         new LBSolrClient.Endpoint("http://host1:8080/solr";), new 
LBSolrClient.Endpoint("http://host2:8080/solr";))
  *     .build();
  * </pre>
@@ -69,7 +72,7 @@ import org.slf4j.MDC;
  * <blockquote>
  *
  * <pre>
- * SolrClient client = new LBHttp2SolrClient.Builder(http2SolrClient,
+ * SolrClient client = new LBHttp2SolrClient.Builder(http2SolrClientBuilder,
  *         new LBSolrClient.Endpoint("http://host1:8080/solr";, "coreA"),
  *         new LBSolrClient.Endpoint("http://host2:8080/solr";, "coreB"))
  *     .build();
@@ -94,35 +97,63 @@ import org.slf4j.MDC;
  *
  * @since solr 8.0
  */
-public class LBHttp2SolrClient<C extends HttpSolrClientBase> extends 
LBSolrClient {
+public class LBHttp2SolrClient<B extends HttpSolrClientBuilderBase<?, ?>> 
extends LBSolrClient {
 
-  protected final C solrClient;
+  private final Map<String, HttpSolrClientBase> urlToClient;
+  private final Set<String> urlParamNames;
+
+  // must synchronize on this when building
+  private final HttpSolrClientBuilderBase<?, ?> solrClientBuilder;
 
-  @SuppressWarnings("unchecked")
   private LBHttp2SolrClient(Builder<?> builder) {
     super(Arrays.asList(builder.solrEndpoints));
-    this.solrClient = (C) builder.solrClient;
+    this.solrClientBuilder = builder.solrClientBuilder;
+
     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 SolrClient getClient(Endpoint endpoint) {
-    return solrClient;
+  protected HttpSolrClientBase getClient(final Endpoint endpoint) {
+    return urlToClient.computeIfAbsent(
+        endpoint.getBaseUrl(),
+        url -> {
+          synchronized (solrClientBuilder) {
+            solrClientBuilder.baseSolrUrl = url;
+            return solrClientBuilder.build();
+          }
+        });
   }
 
   @Override
   public ResponseParser getParser() {
-    return solrClient.getParser();
+    return urlToClient.isEmpty() ? null : 
urlToClient.values().iterator().next().getParser();
   }
 
   @Override
   public RequestWriter getRequestWriter() {
-    return solrClient.getRequestWriter();
+    return urlToClient.isEmpty() ? null : 
urlToClient.values().iterator().next().getRequestWriter();
   }
 
   public Set<String> getUrlParamNames() {
-    return solrClient.getUrlParamNames();
+    return urlParamNames;
+  }
+
+  @Override
+  public void close() {
+    urlToClient.values().forEach(IOUtils::closeQuietly);
+    super.close();
   }
 
   /**
@@ -210,23 +241,18 @@ public class LBHttp2SolrClient<C extends 
HttpSolrClientBase> extends LBSolrClien
       RetryListener listener) {
     String baseUrl = endpoint.toString();
     rsp.server = baseUrl;
-    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);
-    }
+    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;
   }
 
   private void onSuccessfulRequest(
@@ -290,16 +316,28 @@ public class LBHttp2SolrClient<C extends 
HttpSolrClientBase> extends LBSolrClien
     }
   }
 
-  public static class Builder<C extends HttpSolrClientBase> {
+  public static class Builder<B extends HttpSolrClientBuilderBase<?, ?>> {
+
+    private final B solrClientBuilder;
 
-    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;
 
-    public Builder(C solrClient, Endpoint... endpoints) {
-      this.solrClient = solrClient;
+    /**
+     * 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;
       this.solrEndpoints = endpoints;
     }
 
@@ -309,7 +347,7 @@ public class LBHttp2SolrClient<C extends 
HttpSolrClientBase> extends LBSolrClien
      *
      * @param aliveCheckInterval how often to ping for aliveness
      */
-    public Builder<C> setAliveCheckInterval(int aliveCheckInterval, TimeUnit 
unit) {
+    public Builder<B> setAliveCheckInterval(int aliveCheckInterval, TimeUnit 
unit) {
       if (aliveCheckInterval <= 0) {
         throw new IllegalArgumentException(
             "Alive check interval must be " + "positive, specified value = " + 
aliveCheckInterval);
@@ -319,13 +357,13 @@ public class LBHttp2SolrClient<C extends 
HttpSolrClientBase> extends LBSolrClien
     }
 
     /** Sets a default for core or collection based requests. */
-    public Builder<C> withDefaultCollection(String defaultCoreOrCollection) {
+    public Builder<B> withDefaultCollection(String defaultCoreOrCollection) {
       this.defaultCollection = defaultCoreOrCollection;
       return this;
     }
 
-    public LBHttp2SolrClient<C> build() {
-      return new LBHttp2SolrClient<C>(this);
+    public LBHttp2SolrClient<B> build() {
+      return new LBHttp2SolrClient<B>(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 64201b03c13..31c75662b48 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,25 +497,7 @@ public abstract class LBSolrClient extends SolrClient {
   private NamedList<Object> doRequest(Endpoint endpoint, SolrRequest<?> 
solrRequest)
       throws SolrServerException, IOException {
     final var solrClient = getClient(endpoint);
-    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);
+    return solrClient.request(solrRequest, endpoint.getCore());
   }
 
   protected Exception doRequest(
@@ -625,12 +607,7 @@ 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 =
-          doRequest(
-              getClient(zombieEndpoint),
-              zombieEndpoint.getBaseUrl(),
-              effectiveCollection,
-              queryRequest);
+      final var responseRaw = getClient(zombieEndpoint).request(queryRequest, 
effectiveCollection);
       QueryResponse resp = new QueryResponse();
       resp.setResponse(responseRaw);
 
@@ -734,7 +711,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 doRequest(getClient(endpoint), endpoint.getBaseUrl(), 
effectiveCollection, request);
+        return getClient(endpoint).request(request, effectiveCollection);
       } catch (SolrException e) {
         // Server is alive but the request was malformed or invalid
         throw e;
@@ -775,8 +752,7 @@ public abstract class LBSolrClient extends SolrClient {
         ++numServersTried;
         final String effectiveCollection =
             endpoint.getCore() == null ? collection : endpoint.getCore();
-        NamedList<Object> rsp =
-            doRequest(getClient(endpoint), endpoint.getBaseUrl(), 
effectiveCollection, request);
+        NamedList<Object> rsp = getClient(endpoint).request(request, 
effectiveCollection);
         // 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 0846dfefc6c..46b6883f755 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,26 +120,6 @@ 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);
@@ -159,20 +139,6 @@ 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 =
@@ -195,29 +161,19 @@ public class CloudHttp2SolrClientBuilderTest extends 
SolrCloudTestCase {
   public void testHttpClientPreservedInHttp2ClusterStateProvider() throws 
IOException {
     List<String> solrUrls = 
List.of(cluster.getJettySolrRunner(0).getBaseUrl().toString());
 
-    // No httpClient - No internalClientBuilder
-    testHttpClientConsistency(solrUrls, null, null);
-
-    // httpClient - No internalClientBuilder
-    try (Http2SolrClient httpClient = new Http2SolrClient.Builder().build()) {
-      testHttpClientConsistency(solrUrls, httpClient, null);
-    }
+    // without internalClientBuilder
+    testHttpClientConsistency(solrUrls, null);
 
-    // No httpClient - internalClientBuilder
+    // with internalClientBuilder
     Http2SolrClient.Builder internalClientBuilder = new 
Http2SolrClient.Builder();
-    testHttpClientConsistency(solrUrls, null, internalClientBuilder);
+    testHttpClientConsistency(solrUrls, internalClientBuilder);
   }
 
   private void testHttpClientConsistency(
-      List<String> solrUrls,
-      Http2SolrClient httpClient,
-      Http2SolrClient.Builder internalClientBuilder)
-      throws IOException {
+      List<String> solrUrls, Http2SolrClient.Builder internalClientBuilder) 
throws IOException {
     CloudHttp2SolrClient.Builder clientBuilder = new 
CloudHttp2SolrClient.Builder(solrUrls);
 
-    if (httpClient != null) {
-      clientBuilder.withHttpClient(httpClient);
-    } else if (internalClientBuilder != null) {
+    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 b3980ad44bc..1dbfc0e998b 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,7 +34,6 @@ 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;
@@ -154,25 +153,6 @@ 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 61504a052b8..9c2f79ff0a5 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,7 +18,6 @@ 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;
@@ -118,30 +117,28 @@ public class LBHttp2SolrClientIntegrationTest extends 
SolrTestCaseJ4 {
 
   private LBClientHolder client(LBSolrClient.Endpoint... baseSolrEndpoints) {
     if (random().nextBoolean()) {
-      var delegateClient =
+      var delegateClientBuilder =
           new Http2SolrClient.Builder()
               .withConnectionTimeout(1000, TimeUnit.MILLISECONDS)
-              .withIdleTimeout(2000, TimeUnit.MILLISECONDS)
-              .build();
+              .withIdleTimeout(2000, TimeUnit.MILLISECONDS);
       var lbClient =
-          new LBHttp2SolrClient.Builder<>(delegateClient, baseSolrEndpoints)
+          new LBHttp2SolrClient.Builder<>(delegateClientBuilder, 
baseSolrEndpoints)
               .withDefaultCollection(solr[0].getDefaultCollection())
               .setAliveCheckInterval(500, TimeUnit.MILLISECONDS)
               .build();
-      return new LBClientHolder(lbClient, delegateClient);
+      return new LBClientHolder(lbClient, delegateClientBuilder);
     } else {
-      var delegateClient =
+      var delegateClientBuilder =
           new HttpJdkSolrClient.Builder()
               .withConnectionTimeout(1000, TimeUnit.MILLISECONDS)
               .withIdleTimeout(2000, TimeUnit.MILLISECONDS)
-              .withSSLContext(MockTrustManager.ALL_TRUSTING_SSL_CONTEXT)
-              .build();
+              .withSSLContext(MockTrustManager.ALL_TRUSTING_SSL_CONTEXT);
       var lbClient =
-          new LBHttp2SolrClient.Builder<>(delegateClient, baseSolrEndpoints)
+          new LBHttp2SolrClient.Builder<>(delegateClientBuilder, 
baseSolrEndpoints)
               .withDefaultCollection(solr[0].getDefaultCollection())
               .setAliveCheckInterval(500, TimeUnit.MILLISECONDS)
               .build();
-      return new LBClientHolder(lbClient, delegateClient);
+      return new LBClientHolder(lbClient, delegateClientBuilder);
     }
   }
 
@@ -317,9 +314,9 @@ public class LBHttp2SolrClientIntegrationTest extends 
SolrTestCaseJ4 {
   private static class LBClientHolder implements AutoCloseable {
 
     final LBHttp2SolrClient<?> lbClient;
-    final HttpSolrClientBase delegate;
+    final HttpSolrClientBuilderBase<?, ?> delegate;
 
-    LBClientHolder(LBHttp2SolrClient<?> lbClient, HttpSolrClientBase delegate) 
{
+    LBClientHolder(LBHttp2SolrClient<?> lbClient, HttpSolrClientBuilderBase<?, 
?> delegate) {
       this.lbClient = lbClient;
       this.delegate = delegate;
     }
@@ -327,11 +324,6 @@ 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 9d2019309b0..30cee0804b5 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,6 +18,8 @@ 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;
@@ -28,7 +30,6 @@ 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;
@@ -51,11 +52,12 @@ public class LBHttp2SolrClientTest extends SolrTestCase {
     Set<String> urlParamNames = new HashSet<>(2);
     urlParamNames.add("param1");
 
-    try (Http2SolrClient http2SolrClient =
-            new 
Http2SolrClient.Builder(url).withTheseParamNamesInTheUrl(urlParamNames).build();
-        LBHttp2SolrClient<Http2SolrClient> testClient =
-            new LBHttp2SolrClient.Builder<>(http2SolrClient, new 
LBSolrClient.Endpoint(url))
-                .build()) {
+    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()) {
 
       assertArrayEquals(
           "Wrong urlParamNames found in lb client.",
@@ -64,7 +66,7 @@ public class LBHttp2SolrClientTest extends SolrTestCase {
       assertArrayEquals(
           "Wrong urlParamNames found in base client.",
           urlParamNames.toArray(),
-          http2SolrClient.getUrlParamNames().toArray());
+          testClient.getClient(endpoint).getUrlParamNames().toArray());
     }
   }
 
@@ -74,12 +76,11 @@ public class LBHttp2SolrClientTest extends SolrTestCase {
     LBSolrClient.Endpoint ep2 = new 
LBSolrClient.Endpoint("http://endpoint.two";);
     List<LBSolrClient.Endpoint> endpointList = List.of(ep1, ep2);
 
-    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()) {
+    var httpSolrClientBuilder =
+        new MockHttpSolrClientBuilder().withConnectionTimeout(10, 
TimeUnit.SECONDS);
+
+    try (LBHttp2SolrClient<MockHttpSolrClientBuilder> testClient =
+        new LBHttp2SolrClient.Builder<>(httpSolrClientBuilder, ep1, 
ep2).build()) {
 
       String lastEndpoint = null;
       for (int i = 0; i < 10; i++) {
@@ -103,15 +104,14 @@ public class LBHttp2SolrClientTest extends SolrTestCase {
     LBSolrClient.Endpoint ep2 = new 
LBSolrClient.Endpoint("http://endpoint.two";);
     List<LBSolrClient.Endpoint> endpointList = List.of(ep1, ep2);
 
-    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()) {
+    var httpSolrClientBuilder =
+        new MockHttpSolrClientBuilder().withConnectionTimeout(10, 
TimeUnit.SECONDS);
+
+    try (LBHttp2SolrClient<MockHttpSolrClientBuilder> testClient =
+        new LBHttp2SolrClient.Builder<>(httpSolrClientBuilder, ep1, 
ep2).build()) {
 
-      client.basePathToFail = ep1.getBaseUrl();
-      String basePathToSucceed = ep2.getBaseUrl();
+      setEndpointToFail(testClient, ep1);
+      setEndpointToSucceed(testClient, ep2);
       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,
-            basePathToSucceed,
+            ep2.getBaseUrl(),
             response.server);
         checkSynchonousResponseContent(response, qValue);
       }
 
-      client.basePathToFail = ep2.getBaseUrl();
-      basePathToSucceed = ep1.getBaseUrl();
+      setEndpointToFail(testClient, ep2);
+      setEndpointToSucceed(testClient, ep1);
       qValue = "Second time";
 
       for (int i = 0; i < 5; i++) {
@@ -137,21 +137,13 @@ public class LBHttp2SolrClientTest extends SolrTestCase {
         LBSolrClient.Rsp response = testClient.request(req);
         assertEquals(
             "The healthy node 'endpoint one' should have served the request: " 
+ i,
-            basePathToSucceed,
+            ep1.getBaseUrl(),
             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() {
 
@@ -162,28 +154,35 @@ public class LBHttp2SolrClientTest extends SolrTestCase {
     LBSolrClient.Endpoint ep2 = new 
LBSolrClient.Endpoint("http://endpoint.two";);
     List<LBSolrClient.Endpoint> endpointList = List.of(ep1, ep2);
 
-    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()) {
+    var httpSolrClientBuilder =
+        new MockHttpSolrClientBuilder().withConnectionTimeout(10, 
TimeUnit.SECONDS);
+
+    try (LBHttp2SolrClient<MockHttpSolrClientBuilder> testClient =
+        new LBHttp2SolrClient.Builder<>(httpSolrClientBuilder, 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
 
-        String basePathToSucceed;
+        LBSolrClient.Endpoint endpointToSucceed;
+        LBSolrClient.Endpoint endpointToFail;
         if (j == 0) {
-          client.basePathToFail = ep1.getBaseUrl();
-          basePathToSucceed = ep2.getBaseUrl();
+          setEndpointToFail(testClient, ep1);
+          setEndpointToSucceed(testClient, ep2);
+          endpointToSucceed = ep2;
+          endpointToFail = ep1;
         } else {
-          client.basePathToFail = ep2.getBaseUrl();
-          basePathToSucceed = ep1.getBaseUrl();
+          setEndpointToFail(testClient, ep2);
+          setEndpointToSucceed(testClient, ep1);
+          endpointToSucceed = ep1;
+          endpointToFail = ep2;
         }
+        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 to see if it behaves the same every time.
+          // i: we'll try 10 times.  It should behave the same with iter 2-10. 
.
 
           QueryRequest queryRequest = new QueryRequest(new 
MapSolrParams(Map.of("q", "" + i)));
           LBSolrClient.Req req = new LBSolrClient.Req(queryRequest, 
endpointList);
@@ -196,26 +195,26 @@ public class LBHttp2SolrClientTest extends SolrTestCase {
           } catch (TimeoutException | ExecutionException e) {
             fail(iterMessage + " Response ended in failure: " + e);
           }
+
           if (i == 0) {
-            // 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".
+            // When i=0, it must try both endpoints to find success:
             //
-            // 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);
+            //     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());
           } else {
-            // The first endpoint does not give the exception, it doesn't 
retry.
-            assertEquals(iterMessage, 1, client.lastBasePaths.size());
+            // 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());
           }
-          String successBasePath = client.lastBasePaths.remove(0);
-          assertEquals(iterMessage, basePathToSucceed, successBasePath);
+          successEndpointLastBasePaths.clear();
+          failEndpointLastBasePaths.clear();
         }
       }
     }
@@ -227,11 +226,11 @@ public class LBHttp2SolrClientTest extends SolrTestCase {
     LBSolrClient.Endpoint ep2 = new 
LBSolrClient.Endpoint("http://endpoint.two";);
     List<LBSolrClient.Endpoint> endpointList = List.of(ep1, ep2);
 
-    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()) {
+    var httpSolrClientBuilder =
+        new MockHttpSolrClientBuilder().withConnectionTimeout(10, 
TimeUnit.SECONDS);
+
+    try (LBHttp2SolrClient<MockHttpSolrClientBuilder> testClient =
+        new LBHttp2SolrClient.Builder<>(httpSolrClientBuilder, ep1, 
ep2).build()) {
 
       int limit = 10; // For simplicity use an even limit
       List<CompletableFuture<LBSolrClient.Rsp>> responses = new ArrayList<>();
@@ -243,23 +242,17 @@ public class LBHttp2SolrClientTest extends SolrTestCase {
       }
 
       QueryRequest[] queryRequests = new QueryRequest[limit];
-      int numEndpointOne = 0;
-      int numEndpointTwo = 0;
+      List<SolrRequest<?>> lastSolrRequests = lastSolrRequests(testClient, 
ep1, ep2);
+      assertEquals(limit, lastSolrRequests.size());
+
       for (int i = 0; i < limit; i++) {
-        SolrRequest<?> lastSolrReq = client.lastSolrRequests.get(i);
+        SolrRequest<?> lastSolrReq = 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();
@@ -278,15 +271,55 @@ 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!
-      assertEquals(limit, numEndpointOne);
-      assertEquals(0, numEndpointTwo);
+      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, client.lastSolrRequests.size());
-      assertEquals(limit, client.lastCollections.size());
+  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);
     }
   }
 
-  public static class MockHttpSolrClient extends Http2SolrClient {
+  public static class MockHttpSolrClient extends HttpSolrClientBase {
 
     public List<SolrRequest<?>> lastSolrRequests = new ArrayList<>();
 
@@ -294,15 +327,13 @@ public class LBHttp2SolrClientTest extends SolrTestCase {
 
     public List<String> lastCollections = new ArrayList<>();
 
-    public String basePathToFail = null;
+    public boolean allRequestsShallFail;
 
     public String tmpBaseUrl = null;
 
-    protected MockHttpSolrClient(String serverBaseUrl, Builder builder) {
+    public boolean closeCalled;
 
-      // TODO: Consider creating an interface for Http*SolrClient
-      // so mocks can Implement, not Extend, and not actually need to
-      // build an (unused) client
+    protected MockHttpSolrClient(String serverBaseUrl, 
MockHttpSolrClientBuilder builder) {
       super(serverBaseUrl, builder);
     }
 
@@ -312,25 +343,12 @@ public class LBHttp2SolrClientTest extends SolrTestCase {
       lastSolrRequests.add(request);
       lastBasePaths.add(tmpBaseUrl);
       lastCollections.add(collection);
-      if (tmpBaseUrl.equals(basePathToFail)) {
+      if (allRequestsShallFail) {
         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) {
@@ -338,7 +356,7 @@ public class LBHttp2SolrClientTest extends SolrTestCase {
       lastSolrRequests.add(solrRequest);
       lastBasePaths.add(tmpBaseUrl);
       lastCollections.add(collection);
-      if (tmpBaseUrl != null && tmpBaseUrl.equals(basePathToFail)) {
+      if (allRequestsShallFail) {
         cf.completeExceptionally(
             new SolrException(SolrException.ErrorCode.SERVER_ERROR, "We should 
retry this."));
       } else {
@@ -351,5 +369,32 @@ 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