org.apache.solr.client.solrj.impl.LBHttp2SolrClient Maven / Gradle / Ivy
/*
* 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.impl;
import static org.apache.solr.common.params.CommonParams.ADMIN_PATHS;
import java.io.IOException;
import java.net.ConnectException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.Arrays;
import java.util.HashSet;
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.SolrServerException;
import org.apache.solr.client.solrj.request.IsUpdateRequest;
import org.apache.solr.client.solrj.request.RequestWriter;
import org.apache.solr.client.solrj.util.AsyncListener;
import org.apache.solr.client.solrj.util.Cancellable;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.util.NamedList;
import org.slf4j.MDC;
/**
* LBHttp2SolrClient or "LoadBalanced LBHttp2SolrClient" is a load balancing wrapper around {@link
* Http2SolrClient}. This is useful when you have multiple Solr servers and the requests need to be
* Load Balanced among them.
*
* Do NOT use this class for indexing in leader/follower scenarios since documents must be
* sent to the correct leader; no inter-node routing is done.
*
*
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.
*
*
It offers automatic failover when a server goes down, and it detects when the server comes
* back up.
*
*
Load balancing is done using a simple round-robin on the list of servers.
*
*
If a request to a server fails by an IOException due to a connection timeout or read timeout
* then the host is taken off the list of live servers and moved to a 'dead server list' and the
* request is resent to the next live server. This process is continued till it tries all the live
* servers. If at least one server is alive, the request succeeds, and if not it fails.
*
*
*
*
* SolrClient lbHttp2SolrClient = new LBHttp2SolrClient(http2SolrClient, "http://host1:8080/solr/", "http://host2:8080/solr", "http://host2:8080/solr");
*
*
*
*
* This detects if a dead server comes alive automatically. The check is done in fixed intervals in
* a dedicated thread. This interval can be set using {@link
* LBHttp2SolrClient.Builder#setAliveCheckInterval(int)} , the default is set to one minute.
*
* When to use this?
* 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 Load balancing on Wikipedia
*
* @since solr 8.0
*/
public class LBHttp2SolrClient extends LBSolrClient {
private final Http2SolrClient solrClient;
/**
* @deprecated Use {@link LBHttp2SolrClient.Builder} instead
*/
@Deprecated
public LBHttp2SolrClient(Http2SolrClient solrClient, String... baseSolrUrls) {
super(Arrays.asList(baseSolrUrls));
this.solrClient = solrClient;
}
private LBHttp2SolrClient(Builder builder) {
super(Arrays.asList(builder.baseSolrUrls));
this.solrClient = builder.http2SolrClient;
this.aliveCheckIntervalMillis = builder.aliveCheckIntervalMillis;
this.defaultCollection = builder.defaultCollection;
}
/**
* @deprecated Use {@link #getClient(Endpoint)} instead.
*/
@Deprecated
@Override
protected SolrClient getClient(String baseUrl) {
return solrClient;
}
@Override
protected SolrClient getClient(Endpoint endpoint) {
return solrClient;
}
/**
* Note: This setter method is not thread-safe.
*
* @param parser Default Response Parser chosen to parse the response if the parser were not
* specified as part of the request.
* @see org.apache.solr.client.solrj.SolrRequest#getResponseParser()
* @deprecated Pass in a configured {@link Http2SolrClient} instead
*/
@Deprecated
@Override
public void setParser(ResponseParser parser) {
super.setParser(parser);
this.solrClient.setParser(parser);
}
@Override
public ResponseParser getParser() {
return solrClient.getParser();
}
/**
* Choose the {@link RequestWriter} to use.
*
*
By default, {@link BinaryRequestWriter} is used.
*
*
Note: This setter method is not thread-safe.
*
* @deprecated Pass in a configured {@link Http2SolrClient} instead
*/
@Deprecated
@Override
public void setRequestWriter(RequestWriter writer) {
super.setRequestWriter(writer);
this.solrClient.setRequestWriter(writer);
}
@Override
public RequestWriter getRequestWriter() {
return solrClient.getRequestWriter();
}
public Set getUrlParamNames() {
return solrClient.getUrlParamNames();
}
/**
* @deprecated You should instead set this on the passed in Http2SolrClient used by the Builder.
*/
@Deprecated
public void setQueryParams(Set queryParams) {
this.solrClient.setUrlParamNames(queryParams);
}
/**
* This method should be removed as being able to add a query parameter isn't compatible with the
* idea that query params are an immutable property of a solr client.
*
* @deprecated you should instead set this on the passed in Http2SolrClient used by the Builder.
*/
@Deprecated
public void addQueryParams(String queryOnlyParam) {
Set urlParamNames = new HashSet<>(this.solrClient.getUrlParamNames());
urlParamNames.add(queryOnlyParam);
this.solrClient.setUrlParamNames(urlParamNames);
}
/**
* Execute an asynchronous request against a one or more hosts for a given collection.
*
* @param req the wrapped request to perform
* @param asyncListener callers should provide an implementation to handle events: start, success,
* exception
* @return Cancellable allowing the caller to attempt cancellation
* @deprecated Use {@link #requestAsync(Req)}.
*/
@Deprecated
public Cancellable asyncReq(Req req, AsyncListener asyncListener) {
asyncListener.onStart();
CompletableFuture cf =
requestAsync(req)
.whenComplete(
(rsp, t) -> {
if (t != null) {
asyncListener.onFailure(t);
} else {
asyncListener.onSuccess(rsp);
}
});
return () -> cf.cancel(true);
}
/**
* 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
* the list and if unsuccessful tries each in turn until the request is successful. Consequently,
* this method does not actually Load Balance. It is up to the caller to shuffle the List of
* Endpoints if Load Balancing is desired.
*
* @param req the wrapped request to perform
* @return a {@link CompletableFuture} that tracks the progress of the async request.
*/
public CompletableFuture requestAsync(Req req) {
CompletableFuture apiFuture = new CompletableFuture<>();
Rsp rsp = new Rsp();
boolean isNonRetryable =
req.request instanceof IsUpdateRequest || ADMIN_PATHS.contains(req.request.getPath());
ServerIterator it = new ServerIterator(req, zombieServers);
AtomicReference>> currentFuture = new AtomicReference<>();
RetryListener retryListener =
new RetryListener() {
@Override
public void onSuccess(Rsp rsp) {
apiFuture.complete(rsp);
}
@Override
public void onFailure(Exception e, boolean retryReq) {
if (retryReq) {
String url;
try {
url = it.nextOrError(e);
} catch (SolrServerException ex) {
apiFuture.completeExceptionally(e);
return;
}
MDC.put("LBSolrClient.url", url.toString());
if (!apiFuture.isCancelled()) {
CompletableFuture> future =
doAsyncRequest(url, req, rsp, isNonRetryable, it.isServingZombieServer(), this);
currentFuture.set(future);
}
} else {
apiFuture.completeExceptionally(e);
}
}
};
try {
CompletableFuture> future =
doAsyncRequest(
it.nextOrError(),
req,
rsp,
isNonRetryable,
it.isServingZombieServer(),
retryListener);
currentFuture.set(future);
} catch (SolrServerException e) {
apiFuture.completeExceptionally(e);
return apiFuture;
}
apiFuture.exceptionally(
(error) -> {
if (apiFuture.isCancelled()) {
currentFuture.get().cancel(true);
}
return null;
});
return apiFuture;
}
private interface RetryListener {
void onSuccess(Rsp rsp);
void onFailure(Exception e, boolean retryReq);
}
private CompletableFuture> doAsyncRequest(
String endpoint,
Req req,
Rsp rsp,
boolean isNonRetryable,
boolean isZombie,
RetryListener listener) {
String baseUrl = endpoint.toString();
rsp.server = baseUrl;
req.getRequest().setBasePath(baseUrl);
CompletableFuture> future =
((Http2SolrClient) getClient(endpoint)).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;
}
private void onSuccessfulRequest(
NamedList