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 - } } }