org.apache.solr.client.solrj.impl.ConcurrentUpdateSolrClient 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 java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.invoke.MethodHandles;
import java.nio.charset.StandardCharsets;
import java.util.LinkedList;
import java.util.Locale;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentProducer;
import org.apache.http.entity.EntityTemplate;
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.SolrServerException;
import org.apache.solr.client.solrj.request.RequestWriter;
import org.apache.solr.client.solrj.request.UpdateRequest;
import org.apache.solr.client.solrj.util.ClientUtils;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.StringUtils;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.params.UpdateParams;
import org.apache.solr.common.util.ExecutorUtil;
import org.apache.solr.common.util.IOUtils;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.SolrNamedThreadFactory;
import org.apache.solr.common.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
/**
* ConcurrentUpdateSolrClient buffers all added documents and writes
* them into open HTTP connections. This class is thread safe.
*
* Params from {@link UpdateRequest} are converted to http request
* parameters. When params change between UpdateRequests a new HTTP
* request is started.
*
* Although any SolrClient request can be made with this implementation, it is
* only recommended to use ConcurrentUpdateSolrClient with /update
* requests. The class {@link HttpSolrClient} is better suited for the
* query interface.
*/
public class ConcurrentUpdateSolrClient extends SolrClient {
private static final long serialVersionUID = 1L;
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private HttpSolrClient client;
final BlockingQueue queue;
final ExecutorService scheduler;
final Queue runners;
volatile CountDownLatch lock = null; // used to block everything
final int threadCount;
boolean shutdownExecutor = false;
int pollQueueTime = 250;
int stallTime;
private final boolean streamDeletes;
private boolean internalHttpClient;
private volatile Integer connectionTimeout;
private volatile Integer soTimeout;
private volatile boolean closed;
AtomicInteger pollInterrupts;
AtomicInteger pollExits;
AtomicInteger blockLoops;
AtomicInteger emptyQueueLoops;
/**
* Uses the supplied HttpClient to send documents to the Solr server.
*
* @deprecated use {@link ConcurrentUpdateSolrClient#ConcurrentUpdateSolrClient(Builder)} instead, as it is a more extension/subclassing-friendly alternative
*/
@Deprecated
protected ConcurrentUpdateSolrClient(String solrServerUrl,
HttpClient client, int queueSize, int threadCount,
ExecutorService es, boolean streamDeletes) {
this((streamDeletes) ?
new Builder(solrServerUrl)
.withHttpClient(client)
.withQueueSize(queueSize)
.withThreadCount(threadCount)
.withExecutorService(es)
.alwaysStreamDeletes() :
new Builder(solrServerUrl)
.withHttpClient(client)
.withQueueSize(queueSize)
.withThreadCount(threadCount)
.withExecutorService(es)
.neverStreamDeletes());
}
protected ConcurrentUpdateSolrClient(Builder builder) {
this.internalHttpClient = (builder.httpClient == null);
this.client = new HttpSolrClient.Builder(builder.baseSolrUrl)
.withHttpClient(builder.httpClient)
.withConnectionTimeout(builder.connectionTimeoutMillis)
.withSocketTimeout(builder.socketTimeoutMillis)
.build();
this.client.setFollowRedirects(false);
this.queue = new LinkedBlockingQueue<>(builder.queueSize);
this.threadCount = builder.threadCount;
this.runners = new LinkedList<>();
this.streamDeletes = builder.streamDeletes;
this.connectionTimeout = builder.connectionTimeoutMillis;
this.soTimeout = builder.socketTimeoutMillis;
this.stallTime = Integer.getInteger("solr.cloud.client.stallTime", 15000);
if (stallTime < pollQueueTime * 2) {
throw new RuntimeException("Invalid stallTime: " + stallTime + "ms, must be 2x > pollQueueTime " + pollQueueTime);
}
if (builder.executorService != null) {
this.scheduler = builder.executorService;
this.shutdownExecutor = false;
} else {
this.scheduler = ExecutorUtil.newMDCAwareCachedThreadPool(new SolrNamedThreadFactory("concurrentUpdateScheduler"));
this.shutdownExecutor = true;
}
if (log.isDebugEnabled()) {
this.pollInterrupts = new AtomicInteger();
this.pollExits = new AtomicInteger();
this.blockLoops = new AtomicInteger();
this.emptyQueueLoops = new AtomicInteger();
}
}
public Set getQueryParams() {
return this.client.getQueryParams();
}
/**
* Expert Method.
* @param queryParams set of param keys to only send via the query string
*/
public void setQueryParams(Set queryParams) {
this.client.setQueryParams(queryParams);
}
/**
* Opens a connection and sends everything...
*/
@SuppressWarnings({"unchecked"})
class Runner implements Runnable {
volatile Thread thread = null;
volatile boolean inPoll = false;
public Thread getThread() {
return thread;
}
@Override
public void run() {
this.thread = Thread.currentThread();
log.debug("starting runner: {}", this);
// This loop is so we can continue if an element was added to the queue after the last runner exited.
for (;;) {
try {
sendUpdateStream();
} catch (Throwable e) {
if (e instanceof OutOfMemoryError) {
throw (OutOfMemoryError) e;
}
handleError(e);
} finally {
synchronized (runners) {
// check to see if anything else was added to the queue
if (runners.size() == 1 && !queue.isEmpty() && !scheduler.isShutdown()) {
// If there is something else to process, keep last runner alive by staying in the loop.
} else {
runners.remove(this);
if (runners.isEmpty()) {
// notify anyone waiting in blockUntilFinished
runners.notifyAll();
}
break;
}
}
}
}
log.debug("finished: {}", this);
}
public void interruptPoll() {
Thread lthread = thread;
if (inPoll && lthread != null) {
lthread.interrupt();
}
}
//
// Pull from the queue multiple times and streams over a single connection.
// Exits on exception, interruption, or an empty queue to pull from.
//
@SuppressWarnings({"unchecked"})
void sendUpdateStream() throws Exception {
while (!queue.isEmpty()) {
HttpPost method = null;
HttpResponse response = null;
InputStream rspBody = null;
try {
Update update;
notifyQueueAndRunnersIfEmptyQueue();
try {
inPoll = true;
update = queue.poll(pollQueueTime, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
if (log.isDebugEnabled()) pollInterrupts.incrementAndGet();
continue;
} finally {
inPoll = false;
}
if (update == null)
break;
String contentType = client.requestWriter.getUpdateContentType();
final boolean isXml = ClientUtils.TEXT_XML.equals(contentType);
final ModifiableSolrParams origParams = new ModifiableSolrParams(update.getRequest().getParams());
final String origTargetCollection = update.getCollection();
EntityTemplate template = new EntityTemplate(new ContentProducer() {
@Override
public void writeTo(OutputStream out) throws IOException {
if (isXml) {
out.write("".getBytes(StandardCharsets.UTF_8)); // can be anything
}
Update upd = update;
while (upd != null) {
UpdateRequest req = upd.getRequest();
SolrParams currentParams = new ModifiableSolrParams(req.getParams());
if (!origParams.toNamedList().equals(currentParams.toNamedList()) || !StringUtils.equals(origTargetCollection, upd.getCollection())) {
queue.add(upd); // Request has different params or destination core/collection, return to queue
break;
}
client.requestWriter.write(req, out);
if (isXml) {
// check for commit or optimize
SolrParams params = req.getParams();
if (params != null) {
String fmt = null;
if (params.getBool(UpdateParams.OPTIMIZE, false)) {
fmt = " ";
} else if (params.getBool(UpdateParams.COMMIT, false)) {
fmt = " ";
}
if (fmt != null) {
byte[] content = String.format(Locale.ROOT,
fmt, params.getBool(UpdateParams.WAIT_SEARCHER, false)
+ "")
.getBytes(StandardCharsets.UTF_8);
out.write(content);
}
}
}
out.flush();
notifyQueueAndRunnersIfEmptyQueue();
inPoll = true;
try {
while (true) {
try {
upd = queue.poll(pollQueueTime, TimeUnit.MILLISECONDS);
break;
} catch (InterruptedException e) {
if (log.isDebugEnabled()) pollInterrupts.incrementAndGet();
if (!queue.isEmpty()) {
continue;
}
if (log.isDebugEnabled()) pollExits.incrementAndGet();
upd = null;
break;
} finally {
inPoll = false;
}
}
}finally {
inPoll = false;
}
}
if (isXml) {
out.write(" ".getBytes(StandardCharsets.UTF_8));
}
}
});
// The parser 'wt=' and 'version=' params are used instead of the
// original params
ModifiableSolrParams requestParams = new ModifiableSolrParams(origParams);
requestParams.set(CommonParams.WT, client.parser.getWriterType());
requestParams.set(CommonParams.VERSION, client.parser.getVersion());
String basePath = client.getBaseURL();
if (update.getCollection() != null)
basePath += "/" + update.getCollection();
method = new HttpPost(basePath + "/update"
+ requestParams.toQueryString());
org.apache.http.client.config.RequestConfig.Builder requestConfigBuilder = HttpClientUtil.createDefaultRequestConfigBuilder();
if (soTimeout != null) {
requestConfigBuilder.setSocketTimeout(soTimeout);
}
if (connectionTimeout != null) {
requestConfigBuilder.setConnectTimeout(connectionTimeout);
}
method.setConfig(requestConfigBuilder.build());
method.setEntity(template);
method.addHeader("User-Agent", HttpSolrClient.AGENT);
method.addHeader("Content-Type", contentType);
response = client.getHttpClient()
.execute(method, HttpClientUtil.createNewHttpClientRequestContext());
rspBody = response.getEntity().getContent();
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != HttpStatus.SC_OK) {
StringBuilder msg = new StringBuilder();
msg.append(response.getStatusLine().getReasonPhrase());
msg.append("\n\n\n\n");
msg.append("request: ").append(method.getURI());
SolrException solrExc;
NamedList metadata = null;
// parse out the metadata from the SolrException
try {
String encoding = "UTF-8"; // default
if (response.getEntity().getContentType().getElements().length > 0) {
NameValuePair param = response.getEntity().getContentType().getElements()[0].getParameterByName("charset");
if (param != null) {
encoding = param.getValue();
}
}
NamedList
© 2015 - 2025 Weber Informatics LLC | Privacy Policy