This is an automated email from the ASF dual-hosted git repository. dsmiley pushed a commit to branch branch_10_0 in repository https://gitbox.apache.org/repos/asf/solr.git
commit 7ee47d338e8fe0df9befc265f5a7aedc45c8d698 Author: David Smiley <[email protected]> AuthorDate: Fri Nov 28 10:55:46 2025 -0500 SOLR-17161: LBHttp2SolrClient is now LBAsyncSolrClient (#3882) * renamed LBHttp2SolrClient to LBAsyncSolrClient because it's code is mostly for requestAsync. * LBAsyncSolrClient is abstract * new LBJettySolrClient extends LBAsyncSolrClient; only used by HttpShardHandler * LBSolrClient can now be used directly easily, albeit technically remains abstract; took the builder code formerly in LBHttp2SolrClient * CloudHttp2SolrClient internally now only needs an LBSolrClient; not LBHttp2SolrClient (no requestAsync needed). SOLR-17161 (a first part of this issue, adds a "jetty" package) --- .../unreleased/SOLR-17161-LBAsyncSolrClient.yml | 8 ++ .../solr/handler/component/HttpShardHandler.java | 7 +- .../handler/component/HttpShardHandlerFactory.java | 7 +- .../modules/deployment-guide/pages/solrj.adoc | 4 +- .../client/solrj/impl/CloudHttp2SolrClient.java | 12 +- .../solr/client/solrj/impl/Http2SolrClient.java | 6 + .../solr/client/solrj/impl/HttpJdkSolrClient.java | 5 + .../solr/client/solrj/impl/HttpSolrClientBase.java | 5 + ...Http2SolrClient.java => LBAsyncSolrClient.java} | 143 +++------------------ .../solr/client/solrj/impl/LBSolrClient.java | 113 +++++++++++++++- .../solr/client/solrj/jetty/LBJettySolrClient.java | 54 ++++++++ .../solr/client/solrj/jetty/package-info.java | 19 +++ .../impl/CloudHttp2SolrClientBuilderTest.java | 2 +- ...IntegrationTest.java => LB2SolrClientTest.java} | 19 +-- ...rClientTest.java => LBAsyncSolrClientTest.java} | 53 ++------ .../solr/client/solrj/apache/LBHttpSolrClient.java | 3 +- 16 files changed, 260 insertions(+), 200 deletions(-) diff --git a/changelog/unreleased/SOLR-17161-LBAsyncSolrClient.yml b/changelog/unreleased/SOLR-17161-LBAsyncSolrClient.yml new file mode 100644 index 00000000000..f722b2ffb46 --- /dev/null +++ b/changelog/unreleased/SOLR-17161-LBAsyncSolrClient.yml @@ -0,0 +1,8 @@ +# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc +title: LBAsyncSolrClient (renamed from LBHttp2SolrClient) & LBSolrClient (improved) & LBJettySolrClient (new, clarifying impl). +type: other # added, changed, fixed, deprecated, removed, dependency_update, security, other +authors: + - name: David Smiley +links: + - name: SOLR-17161 + url: https://issues.apache.org/jira/browse/SOLR-17161 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 b58da55859a..788d985a79e 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 @@ -33,8 +33,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import net.jcip.annotations.NotThreadSafe; import org.apache.solr.client.solrj.SolrRequest; import org.apache.solr.client.solrj.SolrResponse; -import org.apache.solr.client.solrj.impl.Http2SolrClient; -import org.apache.solr.client.solrj.impl.LBHttp2SolrClient; +import org.apache.solr.client.solrj.impl.LBAsyncSolrClient; import org.apache.solr.client.solrj.impl.LBSolrClient; import org.apache.solr.client.solrj.request.QueryRequest; import org.apache.solr.client.solrj.routing.NoOpReplicaListTransformer; @@ -98,7 +97,7 @@ public class HttpShardHandler extends ShardHandler { private final AtomicBoolean canceled = new AtomicBoolean(false); private final Map<String, List<String>> shardToURLs; - protected LBHttp2SolrClient<Http2SolrClient> lbClient; + protected LBAsyncSolrClient lbClient; public HttpShardHandler(HttpShardHandlerFactory httpShardHandlerFactory) { this.httpShardHandlerFactory = httpShardHandlerFactory; @@ -247,7 +246,7 @@ public class HttpShardHandler extends ShardHandler { * @param sreq the request to make * @param shard the shard to address * @param params request parameters - * @param lbReq the load balanced request suitable for LBHttp2SolrClient + * @param lbReq the load balanced request * @param ssr the response collector part 1 * @param srsp the shard response collector * @param startTimeNS the time at which the request was initiated, likely just prior to calling 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 0a78e179019..0daec7a4825 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 @@ -32,9 +32,10 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.impl.Http2SolrClient; -import org.apache.solr.client.solrj.impl.LBHttp2SolrClient; +import org.apache.solr.client.solrj.impl.LBAsyncSolrClient; import org.apache.solr.client.solrj.impl.LBSolrClient; import org.apache.solr.client.solrj.impl.SolrHttpConstants; +import org.apache.solr.client.solrj.jetty.LBJettySolrClient; import org.apache.solr.client.solrj.request.QueryRequest; import org.apache.solr.client.solrj.routing.AffinityReplicaListTransformerFactory; import org.apache.solr.client.solrj.routing.ReplicaListTransformer; @@ -82,7 +83,7 @@ public class HttpShardHandlerFactory extends ShardHandlerFactory protected volatile Http2SolrClient defaultClient; protected InstrumentedHttpListenerFactory httpListenerFactory; - protected LBHttp2SolrClient<Http2SolrClient> loadbalancer; + protected LBAsyncSolrClient loadbalancer; int corePoolSize = 0; int maximumPoolSize = Integer.MAX_VALUE; @@ -307,7 +308,7 @@ public class HttpShardHandlerFactory extends ShardHandlerFactory .withMaxConnectionsPerHost(maxConnectionsPerHost) .build(); this.defaultClient.addListenerFactory(this.httpListenerFactory); - this.loadbalancer = new LBHttp2SolrClient.Builder<Http2SolrClient>(defaultClient).build(); + this.loadbalancer = new LBJettySolrClient.Builder(defaultClient).build(); initReplicaListTransformers(getParameter(args, "replicaRouting", null, sb)); diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/solrj.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/solrj.adoc index e61f17567f7..5582621ec7b 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/solrj.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/solrj.adoc @@ -97,7 +97,7 @@ Requests are sent in the form of {solr-javadocs}/solrj/org/apache/solr/client/so - {solr-javadocs}/solrj/org/apache/solr/client/solrj/impl/Http2SolrClient.html[`Http2SolrClient`] - a general purpose client based on Jetty HttpClient. Supports HTTP/2 and HTTP/1.1, async, non-blocking. Most used & tested. - {solr-javadocs}/solrj/org/apache/solr/client/solrj/impl/HttpJdkSolrClient.html[`HttpJdkSolrClient`] - a general purpose client based on JDK HttpClient. Supports HTTP/2 and HTTP/1.1, async, non-blocking. Has no dependencies. -- {solr-javadocs}/solrj/org/apache/solr/client/solrj/impl/LBHttp2SolrClient.html[`LBHttp2SolrClient`] - an internal client that delegates to other clients pointed at different URLs for fail-over/availability. Adjusts the list of "in-service" nodes based on node health. +- {solr-javadocs}/solrj/org/apache/solr/client/solrj/impl/LBSolrClient.html[`LBSolrClient`] - an internal client that delegates to other clients pointed at different URLs for fail-over/availability. Adjusts the list of "in-service" nodes based on node health. - {solr-javadocs}/solrj/org/apache/solr/client/solrj/impl/CloudSolrClient.html[`CloudSolrClient`] - the ideal client for SolrCloud. Using the "cluster state", it routes requests to the optimal nodes, including splitting out the documents in an UpdateRequest to different nodes. - {solr-javadocs}/solrj/org/apache/solr/client/solrj/impl/ConcurrentUpdateHttp2SolrClient.html[`ConcurrentUpdateHttp2SolrClient`] - geared towards indexing-centric workloads. Buffers documents internally before sending larger batches to Solr. @@ -116,7 +116,7 @@ A few notable exceptions to this are described below: - *Http2SolrClient* - Users of `Http2SolrClient` may choose to skip providing a root URL to their client, in favor of specifying the URL as an argument for the `Http2SolrClient.requestWithBaseUrl` method. Calling any other `request` methods on a URL-less `Http2SolrClient` will result in an `IllegalArgumentException`. -- *LBHttp2SolrClient* - Solr's "load balancing" client is frequently used to round-robin requests across a set of replicas or cores. +- *LBSolrClient* - Solr's "load balancing" client is frequently used to round-robin requests across a set of replicas or cores. URLs are still expected to point to the Solr root (i.e. "/solr"), but to support this use-case the URLs are often supplemented by an additional parameter to specify the targeted core. Alternatively, some "load balancing" methods make use of an `Endpoint` abstraction to provide this URL and core information in a more structured way. - *CloudSolrClient* - Like many clients, CloudSolrClient accepts a series of URLs pointing to the Solr root path (i.e. "/solr"). 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 4fc82b630d3..6886952c25c 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 @@ -35,7 +35,7 @@ import org.slf4j.LoggerFactory; /** * SolrJ client class to communicate with SolrCloud using an Http/2-capable Solr Client. Instances * of this class communicate with Zookeeper to discover Solr endpoints for SolrCloud collections, - * and then use the {@link LBHttp2SolrClient} to issue requests. + * and then use the {@link LBSolrClient} to issue requests. * * @since solr 8.0 */ @@ -44,7 +44,7 @@ public class CloudHttp2SolrClient extends CloudSolrClient { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private final ClusterStateProvider stateProvider; - private final LBHttp2SolrClient<HttpSolrClientBase> lbClient; + private final LBSolrClient lbClient; private final HttpSolrClientBase myClient; private final boolean clientIsInternal; @@ -90,7 +90,7 @@ public class CloudHttp2SolrClient extends CloudSolrClient { // locks. this.locks = objectList(builder.parallelCacheRefreshesLocks); - this.lbClient = new LBHttp2SolrClient.Builder<>(myClient).build(); + this.lbClient = builder.createOrGetLbClient(myClient); } private HttpSolrClientBase createOrGetHttpClientFromBuilder(Builder builder) { @@ -160,7 +160,7 @@ public class CloudHttp2SolrClient extends CloudSolrClient { } @Override - public LBHttp2SolrClient<?> getLbClient() { + public LBSolrClient getLbClient() { return lbClient; } @@ -452,5 +452,9 @@ public class CloudHttp2SolrClient extends CloudSolrClient { return new CloudHttp2SolrClient(this); } + + protected LBSolrClient createOrGetLbClient(HttpSolrClientBase myClient) { + return myClient.createLBSolrClient(); + } } } 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 83b24671bbe..d8005979e90 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 @@ -47,6 +47,7 @@ import org.apache.solr.client.solrj.SolrRequest; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.embedded.SSLConfig; import org.apache.solr.client.solrj.impl.HttpListenerFactory.RequestResponseListener; +import org.apache.solr.client.solrj.jetty.LBJettySolrClient; import org.apache.solr.client.solrj.request.RequestWriter; import org.apache.solr.client.solrj.request.UpdateRequest; import org.apache.solr.client.solrj.util.ClientUtils; @@ -638,6 +639,11 @@ public class Http2SolrClient extends HttpSolrClientBase { } } + @Override + protected LBSolrClient createLBSolrClient() { + return new LBJettySolrClient.Builder(this).build(); + } + @Override public HttpSolrClientBuilderBase<?, ?> builder() { return new Http2SolrClient.Builder().withHttpClient(this); 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 d65a8fb9681..4e0472cc2d8 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 @@ -554,6 +554,11 @@ public class HttpJdkSolrClient extends HttpSolrClientBase { return new HttpJdkSolrClient.Builder().withHttpClient(this); } + @Override + protected LBSolrClient createLBSolrClient() { + return new LBSolrClient.Builder<>(this).build(); + } + public static class Builder extends HttpSolrClientBuilderBase<HttpJdkSolrClient.Builder, HttpJdkSolrClient> { diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpSolrClientBase.java b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpSolrClientBase.java index 0fa2936cbfc..ffed7779fb4 100644 --- a/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpSolrClientBase.java +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/impl/HttpSolrClientBase.java @@ -101,6 +101,11 @@ public abstract class HttpSolrClientBase extends SolrClient { public abstract HttpSolrClientBuilderBase<?, ?> builder(); + /** + * @lucene.internal + */ + protected abstract LBSolrClient createLBSolrClient(); + protected String getRequestUrl(SolrRequest<?> solrRequest, String collection) throws MalformedURLException { return ClientUtils.buildRequestUrl(solrRequest, serverBaseUrl, collection); 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/LBAsyncSolrClient.java similarity index 58% rename from solr/solrj/src/java/org/apache/solr/client/solrj/impl/LBHttp2SolrClient.java rename to solr/solrj/src/java/org/apache/solr/client/solrj/impl/LBAsyncSolrClient.java index 89ffbd707a0..41225ffaf25 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/LBAsyncSolrClient.java @@ -20,109 +20,32 @@ import java.io.IOException; import java.net.ConnectException; import java.net.SocketException; import java.net.SocketTimeoutException; -import java.util.Arrays; -import java.util.Set; import java.util.concurrent.CompletableFuture; -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.SolrRequest; import org.apache.solr.client.solrj.SolrRequest.SolrRequestType; import org.apache.solr.client.solrj.SolrServerException; -import org.apache.solr.client.solrj.request.RequestWriter; import org.apache.solr.common.SolrException; 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. - * - * <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. - * - * <p>In SolrCloud (leader/replica) scenarios, it is usually better to use {@link CloudSolrClient}, - * but this class may be used for updates because the server will forward them to the appropriate - * leader. - * - * <p>It offers automatic failover when a server goes down, and it detects when the server comes - * back up. - * - * <p>Load balancing is done using a simple round-robin on the list of endpoints. Endpoint URLs are - * expected to point to the Solr "root" path (i.e. "/solr"). - * - * <blockquote> - * - * <pre> - * SolrClient client = new LBHttp2SolrClient.Builder(http2SolrClient, - * new LBSolrClient.Endpoint("http://host1:8080/solr"), new LBSolrClient.Endpoint("http://host2:8080/solr")) - * .build(); - * </pre> - * - * </blockquote> - * - * Users who wish to balance traffic across a specific set of replicas or cores may specify each - * endpoint as a root-URL and core-name pair. For example: - * - * <blockquote> - * - * <pre> - * SolrClient client = new LBHttp2SolrClient.Builder(http2SolrClient, - * new LBSolrClient.Endpoint("http://host1:8080/solr", "coreA"), - * new LBSolrClient.Endpoint("http://host2:8080/solr", "coreB")) - * .build(); - * </pre> - * - * </blockquote> - * - * <p>If a request to an endpoint fails by an IOException due to a connection timeout or read - * timeout then the host is taken off the list of live endpoints and moved to a 'dead endpoint list' - * and the request is resent to the next live endpoint. This process is continued till it tries all - * the live endpoints. If at least one endpoint is alive, the request succeeds, and if not it fails. - * - * <p>Dead endpoints are periodically healthchecked on a fixed interval controlled by {@link - * LBHttp2SolrClient.Builder#setAliveCheckInterval(int, TimeUnit)}. The default is set to one - * minute. - * - * <p><b>When to use this?</b><br> - * This can be used as a software load balancer when you do not wish to set up an external load - * balancer. Alternatives to this code are to use a dedicated hardware load balancer or using Apache - * httpd with mod_proxy_balancer as a load balancer. See <a - * href="http://en.wikipedia.org/wiki/Load_balancing_(computing)">Load balancing on Wikipedia</a> - * - * @since solr 8.0 - */ -public class LBHttp2SolrClient<C extends HttpSolrClientBase> extends LBSolrClient { +/** A {@link LBSolrClient} adding {@link #requestAsync(Req)}. */ +public abstract class LBAsyncSolrClient extends LBSolrClient { + // formerly known as LBHttp2SolrClient, using Http2SolrClient (jetty) - protected final C solrClient; + protected final HttpSolrClientBase solrClient; - @SuppressWarnings("unchecked") - private LBHttp2SolrClient(Builder<?> builder) { - super(Arrays.asList(builder.solrEndpoints)); - this.solrClient = (C) builder.solrClient; - this.aliveCheckIntervalMillis = builder.aliveCheckIntervalMillis; - this.defaultCollection = builder.defaultCollection; + protected LBAsyncSolrClient(Builder<?> builder) { + super(builder); + this.solrClient = builder.getSolrClient(); } @Override - protected SolrClient getClient(Endpoint endpoint) { + protected HttpSolrClientBase getClient(Endpoint endpoint) { return solrClient; } - @Override - public ResponseParser getParser() { - return solrClient.getParser(); - } - - @Override - public RequestWriter getRequestWriter() { - return solrClient.getRequestWriter(); - } - - public Set<String> getUrlParamNames() { - return solrClient.getUrlParamNames(); - } - /** * Execute an asynchronous request against one or more hosts for a given collection. The passed-in * Req object includes a List of Endpoints. This method always begins with the first Endpoint in @@ -214,10 +137,9 @@ 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())); + requestAsyncWithUrl(getClient(endpoint), baseUrl, req.getRequest()); future.whenComplete( (result, throwable) -> { if (!future.isCompletedExceptionally()) { @@ -228,11 +150,15 @@ public class LBHttp2SolrClient<C extends HttpSolrClientBase> extends LBSolrClien }); return future; } catch (SolrServerException | IOException e) { - // Unreachable, since 'requestWithBaseUrl' above is running the request asynchronously + // Unreachable, since 'requestAsyncWithUrl' above is running the request asynchronously throw new RuntimeException(e); } } + protected abstract CompletableFuture<NamedList<Object>> requestAsyncWithUrl( + SolrClient client, String baseUrl, SolrRequest<?> request) + throws SolrServerException, IOException; + private void onSuccessfulRequest( NamedList<Object> result, Endpoint endpoint, @@ -293,43 +219,4 @@ public class LBHttp2SolrClient<C extends HttpSolrClientBase> extends LBSolrClien listener.onFailure(new SolrServerException(e), false); } } - - 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; - - public Builder(C solrClient, Endpoint... endpoints) { - this.solrClient = solrClient; - this.solrEndpoints = endpoints; - } - - /** - * LBHttpSolrServer keeps pinging the dead servers at fixed interval to find if it is alive. Use - * this to set that interval - * - * @param aliveCheckInterval how often to ping for aliveness - */ - public Builder<C> setAliveCheckInterval(int aliveCheckInterval, TimeUnit unit) { - if (aliveCheckInterval <= 0) { - throw new IllegalArgumentException( - "Alive check interval must be " + "positive, specified value = " + aliveCheckInterval); - } - this.aliveCheckIntervalMillis = TimeUnit.MILLISECONDS.convert(aliveCheckInterval, unit); - return this; - } - - /** Sets a default for core or collection based requests. */ - public Builder<C> withDefaultCollection(String defaultCoreOrCollection) { - this.defaultCollection = defaultCoreOrCollection; - return 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 c71d0210bec..1b9fbc5a3c6 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 @@ -61,6 +61,61 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; +/** + * This "LoadBalanced Http Solr Client" is a load balancing wrapper around an 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. + * + * <p>In SolrCloud (leader/replica) scenarios, it is usually better to use {@link CloudSolrClient}, + * but this class may be used for updates because the server will forward them to the appropriate + * leader. + * + * <p>It offers automatic failover when a server goes down, and it detects when the server comes + * back up. + * + * <p>Load balancing is done using a simple round-robin on the list of endpoints. Endpoint URLs are + * expected to point to the Solr "root" path (i.e. "/solr"). + * + * <blockquote> + * + * <pre> + * SolrClient client = new LBSolrClient.Builder(http2SolrClient, + * new LBSolrClient.Endpoint("http://host1:8080/solr"), new LBSolrClient.Endpoint("http://host2:8080/solr")) + * .build(); + * </pre> + * + * </blockquote> + * + * Users who wish to balance traffic across a specific set of replicas or cores may specify each + * endpoint as a root-URL and core-name pair. For example: + * + * <blockquote> + * + * <pre> + * SolrClient client = new LBSolrClient.Builder(http2SolrClient, + * new LBSolrClient.Endpoint("http://host1:8080/solr", "coreA"), + * new LBSolrClient.Endpoint("http://host2:8080/solr", "coreB")) + * .build(); + * </pre> + * + * </blockquote> + * + * <p>If a request to an endpoint fails by an IOException due to a connection timeout or read + * timeout then the host is taken off the list of live endpoints and moved to a 'dead endpoint list' + * and the request is resent to the next live endpoint. This process is continued till it tries all + * the live endpoints. If at least one endpoint is alive, the request succeeds, and if not it fails. + * + * <p>Dead endpoints are periodically healthchecked on a fixed interval controlled by {@code + * LBSolrClient.Builder#setAliveCheckInterval(int, TimeUnit)}. The default is set to one minute. + * + * <p><b>When to use this?</b><br> + * This can be used as a software load balancer when you do not wish to set up an external load + * balancer. Alternatives to this code are to use a dedicated hardware load balancer or using Apache + * httpd with mod_proxy_balancer as a load balancer. See <a + * href="http://en.wikipedia.org/wiki/Load_balancing_(computing)">Load balancing on Wikipedia</a> + */ public abstract class LBSolrClient extends SolrClient { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @@ -110,6 +165,54 @@ public abstract class LBSolrClient extends SolrClient { solrQuery.setDistrib(false); } + public static class Builder<C extends HttpSolrClientBase> { + + private final C solrClient; + private final 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; + this.solrEndpoints = endpoints; + } + + /** + * LBHttpSolrServer keeps pinging the dead servers at fixed interval to find if it is alive. Use + * this to set that interval + * + * @param aliveCheckInterval how often to ping for aliveness + */ + public Builder<C> setAliveCheckInterval(int aliveCheckInterval, TimeUnit unit) { + if (aliveCheckInterval <= 0) { + throw new IllegalArgumentException( + "Alive check interval must be " + "positive, specified value = " + aliveCheckInterval); + } + this.aliveCheckIntervalMillis = TimeUnit.MILLISECONDS.convert(aliveCheckInterval, unit); + return this; + } + + /** Sets a default for core or collection based requests. */ + public Builder<C> withDefaultCollection(String defaultCoreOrCollection) { + this.defaultCollection = defaultCoreOrCollection; + return this; + } + + public C getSolrClient() { + return solrClient; + } + + public LBSolrClient build() { + return new LBSolrClient(this) { + @Override + protected SolrClient getClient(Endpoint endpoint) { + return solrClient; + } + }; + } + } + /** * A Solr endpoint for {@link LBSolrClient} to include in its load-balancing * @@ -401,7 +504,15 @@ public abstract class LBSolrClient extends SolrClient { } } - public LBSolrClient(List<Endpoint> solrEndpoints) { + protected LBSolrClient(Builder<?> builder) { + this(Arrays.asList(builder.solrEndpoints)); + this.aliveCheckIntervalMillis = builder.aliveCheckIntervalMillis; + this.defaultCollection = builder.defaultCollection; + this.requestWriter = builder.solrClient.getRequestWriter(); + this.parser = builder.solrClient.getParser(); + } + + protected LBSolrClient(List<Endpoint> solrEndpoints) { if (!solrEndpoints.isEmpty()) { for (Endpoint s : solrEndpoints) { EndpointWrapper wrapper = createServerWrapper(s); diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/jetty/LBJettySolrClient.java b/solr/solrj/src/java/org/apache/solr/client/solrj/jetty/LBJettySolrClient.java new file mode 100644 index 00000000000..4790fabcf3a --- /dev/null +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/jetty/LBJettySolrClient.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.client.solrj.jetty; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import org.apache.solr.client.solrj.SolrClient; +import org.apache.solr.client.solrj.SolrRequest; +import org.apache.solr.client.solrj.SolrServerException; +import org.apache.solr.client.solrj.impl.Http2SolrClient; +import org.apache.solr.client.solrj.impl.LBAsyncSolrClient; +import org.apache.solr.client.solrj.impl.LBSolrClient; +import org.apache.solr.common.util.NamedList; + +/** An {@link LBSolrClient} based on Jetty HttpClient, supporting async. */ +public class LBJettySolrClient extends LBAsyncSolrClient { + + protected LBJettySolrClient(Builder builder) { + super(builder); + } + + public static class Builder extends LBSolrClient.Builder<Http2SolrClient> { + + public Builder(Http2SolrClient solrClient, Endpoint... endpoints) { + super(solrClient, endpoints); + } + + @Override + public LBJettySolrClient build() { + return new LBJettySolrClient(this); + } + } + + @Override + protected CompletableFuture<NamedList<Object>> requestAsyncWithUrl( + SolrClient client, String baseUrl, SolrRequest<?> request) + throws SolrServerException, IOException { + return ((Http2SolrClient) client).requestWithBaseUrl(baseUrl, c -> c.requestAsync(request)); + } +} diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/jetty/package-info.java b/solr/solrj/src/java/org/apache/solr/client/solrj/jetty/package-info.java new file mode 100644 index 00000000000..cd7607f2790 --- /dev/null +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/jetty/package-info.java @@ -0,0 +1,19 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** SolrJ code that depends on Jetty client / HttpClient. */ +package org.apache.solr.client.solrj.jetty; 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 e4292ee3185..6f8f8587b51 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 @@ -214,7 +214,7 @@ public class CloudHttp2SolrClientBuilderTest extends SolrCloudTestCase { Collections.singletonList(ANY_ZK_HOST), Optional.of(ANY_CHROOT)) .build()) { assertTrue(createdClient.getHttpClient() instanceof Http2SolrClient); - assertTrue(createdClient.getLbClient().solrClient instanceof Http2SolrClient); + assertTrue(createdClient.getLbClient().getClient(null) instanceof Http2SolrClient); } } 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/LB2SolrClientTest.java similarity index 94% rename from solr/solrj/src/test/org/apache/solr/client/solrj/impl/LBHttp2SolrClientIntegrationTest.java rename to solr/solrj/src/test/org/apache/solr/client/solrj/impl/LB2SolrClientTest.java index 3f24984f90e..ed0975b5181 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/LB2SolrClientTest.java @@ -18,7 +18,6 @@ package org.apache.solr.client.solrj.impl; import java.io.IOException; import java.io.UncheckedIOException; -import java.lang.invoke.MethodHandles; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -41,18 +40,14 @@ import org.apache.solr.util.LogLevel; import org.apache.solr.util.LogListener; import org.junit.AfterClass; import org.junit.BeforeClass; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** - * Test for LBHttp2SolrClient + * Integration test for {@link LBSolrClient} * * @since solr 1.4 */ @LogLevel("org.apache.solr.client.solrj.impl=DEBUG") -public class LBHttp2SolrClientIntegrationTest extends SolrTestCaseJ4 { - - private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); +public class LB2SolrClientTest extends SolrTestCaseJ4 { SolrInstance[] solr = new SolrInstance[3]; @@ -124,7 +119,7 @@ public class LBHttp2SolrClientIntegrationTest extends SolrTestCaseJ4 { .withIdleTimeout(2000, TimeUnit.MILLISECONDS) .build(); var lbClient = - new LBHttp2SolrClient.Builder<>(delegateClient, baseSolrEndpoints) + new LBSolrClient.Builder<>(delegateClient, baseSolrEndpoints) .withDefaultCollection(solr[0].getDefaultCollection()) .setAliveCheckInterval(500, TimeUnit.MILLISECONDS) .build(); @@ -137,7 +132,7 @@ public class LBHttp2SolrClientIntegrationTest extends SolrTestCaseJ4 { .withSSLContext(MockTrustManager.ALL_TRUSTING_SSL_CONTEXT) .build(); var lbClient = - new LBHttp2SolrClient.Builder<>(delegateClient, baseSolrEndpoints) + new LBSolrClient.Builder<>(delegateClient, baseSolrEndpoints) .withDefaultCollection(solr[0].getDefaultCollection()) .setAliveCheckInterval(500, TimeUnit.MILLISECONDS) .build(); @@ -316,10 +311,10 @@ public class LBHttp2SolrClientIntegrationTest extends SolrTestCaseJ4 { private static class LBClientHolder implements AutoCloseable { - final LBHttp2SolrClient<?> lbClient; - final HttpSolrClientBase delegate; + final LBSolrClient lbClient; + final SolrClient delegate; // http - LBClientHolder(LBHttp2SolrClient<?> lbClient, HttpSolrClientBase delegate) { + LBClientHolder(LBSolrClient lbClient, SolrClient delegate) { this.lbClient = lbClient; this.delegate = delegate; } 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/LBAsyncSolrClientTest.java similarity index 87% rename from solr/solrj/src/test/org/apache/solr/client/solrj/impl/LBHttp2SolrClientTest.java rename to solr/solrj/src/test/org/apache/solr/client/solrj/impl/LBAsyncSolrClientTest.java index 9d2019309b0..a83bd793b49 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/LBAsyncSolrClientTest.java @@ -19,10 +19,8 @@ package org.apache.solr.client.solrj.impl; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -31,42 +29,15 @@ 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.jetty.LBJettySolrClient; import org.apache.solr.client.solrj.request.QueryRequest; import org.apache.solr.common.SolrException; import org.apache.solr.common.params.MapSolrParams; import org.apache.solr.common.util.NamedList; import org.junit.Test; -/** Test the LBHttp2SolrClient. */ -public class LBHttp2SolrClientTest extends SolrTestCase { - - /** - * Test method for {@link LBHttp2SolrClient.Builder} that validates that the query param keys - * passed in by the base <code>Http2SolrClient - * </code> instance are used by the LBHttp2SolrClient. - */ - @Test - public void testLBHttp2SolrClientWithTheseParamNamesInTheUrl() { - String url = "http://127.0.0.1:8080"; - 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()) { - - assertArrayEquals( - "Wrong urlParamNames found in lb client.", - urlParamNames.toArray(), - testClient.getUrlParamNames().toArray()); - assertArrayEquals( - "Wrong urlParamNames found in base client.", - urlParamNames.toArray(), - http2SolrClient.getUrlParamNames().toArray()); - } - } +/** Test the LBAsyncSolrClient. */ +public class LBAsyncSolrClientTest extends SolrTestCase { @Test public void testSynchronous() throws Exception { @@ -76,10 +47,9 @@ public class LBHttp2SolrClientTest extends SolrTestCase { 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 testClient = new LBJettySolrClient.Builder(client, ep1, ep2).build()) { String lastEndpoint = null; for (int i = 0; i < 10; i++) { @@ -105,10 +75,9 @@ public class LBHttp2SolrClientTest extends SolrTestCase { 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 testClient = new LBJettySolrClient.Builder(client, ep1, ep2).build()) { client.basePathToFail = ep1.getBaseUrl(); String basePathToSucceed = ep2.getBaseUrl(); @@ -164,10 +133,9 @@ public class LBHttp2SolrClientTest extends SolrTestCase { 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 testClient = new LBJettySolrClient.Builder(client, ep1, ep2).build()) { for (int j = 0; j < 2; j++) { // first time Endpoint One will return error code 500. @@ -230,8 +198,7 @@ public class LBHttp2SolrClientTest extends SolrTestCase { 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 testClient = new LBJettySolrClient.Builder(client, ep1, ep2).build()) { int limit = 10; // For simplicity use an even limit List<CompletableFuture<LBSolrClient.Rsp>> responses = new ArrayList<>(); diff --git a/solr/test-framework/src/java/org/apache/solr/client/solrj/apache/LBHttpSolrClient.java b/solr/test-framework/src/java/org/apache/solr/client/solrj/apache/LBHttpSolrClient.java index b926808ddb9..bac755dc692 100644 --- a/solr/test-framework/src/java/org/apache/solr/client/solrj/apache/LBHttpSolrClient.java +++ b/solr/test-framework/src/java/org/apache/solr/client/solrj/apache/LBHttpSolrClient.java @@ -26,7 +26,6 @@ import org.apache.http.client.HttpClient; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.impl.CloudSolrClient; import org.apache.solr.client.solrj.impl.JavaBinResponseParser; -import org.apache.solr.client.solrj.impl.LBHttp2SolrClient; import org.apache.solr.client.solrj.impl.LBSolrClient; import org.apache.solr.common.params.ModifiableSolrParams; @@ -95,7 +94,7 @@ import org.apache.solr.common.params.ModifiableSolrParams; * href="http://en.wikipedia.org/wiki/Load_balancing_(computing)">Load balancing on Wikipedia</a> * * @since solr 1.4 - * @deprecated Please use {@link LBHttp2SolrClient} + * @deprecated Not supporting Apache HttpClient based implementations anymore */ @Deprecated(since = "9.0") public class LBHttpSolrClient extends LBSolrClient {
