/** * 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.OutputStream; import java.io.OutputStreamWriter; import java.net.MalformedURLException; import java.util.LinkedList; import java.util.Queue; import java.util.concurrent.*; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.httpclient.methods.PostMethod; import org.apache.commons.httpclient.methods.RequestEntity; import org.apache.solr.client.solrj.SolrRequest; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.request.UpdateRequest; import org.apache.solr.client.solrj.util.ClientUtils; import org.apache.solr.common.params.SolrParams; import org.apache.solr.common.params.UpdateParams; import org.apache.solr.common.util.NamedList; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * {@link StreamingUpdateSolrServer} buffers all added documents and writes them * into open HTTP connections. This class is thread safe. * * Although any SolrServer request can be made with this implementation, * it is only recommended to use the {@link StreamingUpdateSolrServer} with * /update requests. The query interface is better suited for * * @version $Id: CommonsHttpSolrServer.java 724175 2008-12-07 19:07:11Z ryan $ * @since solr 1.4 */ public class StreamingUpdateSolrServer extends CommonsHttpSolrServer { static final Logger log = LoggerFactory.getLogger( StreamingUpdateSolrServer.class ); final BlockingQueue<UpdateRequest> queue; final ExecutorService scheduler = Executors.newCachedThreadPool(); final String updateUrl = "/update"; final Queue<Runner> runners; volatile CountDownLatch lock = null; // used to block everything final int threadCount; /** * Uses an internal MultiThreadedHttpConnectionManager to manage http connections * * @param solrServerUrl The Solr server URL * @param queueSize The buffer size before the documents are sent to the server * @param threadCount The number of background threads used to empty the queue * @throws MalformedURLException */ public StreamingUpdateSolrServer(String solrServerUrl, int queueSize, int threadCount) throws MalformedURLException { this(solrServerUrl, null, queueSize, threadCount); } /** * Uses the supplied HttpClient to send documents to the Solr server, the HttpClient should be instantiated using a * MultiThreadedHttpConnectionManager. */ public StreamingUpdateSolrServer(String solrServerUrl, HttpClient client, int queueSize, int threadCount) throws MalformedURLException { super(solrServerUrl, client); queue = new LinkedBlockingQueue<UpdateRequest>(queueSize); this.threadCount = threadCount; runners = new LinkedList<Runner>(); } /** * Opens a connection and sends everything... */ class Runner implements Runnable { final Lock runnerLock = new ReentrantLock(); public void run() { runnerLock.lock(); // info is ok since this should only happen once for each thread log.info( "starting runner: {}" , this ); PostMethod method = null; try { do { try { RequestEntity request = new RequestEntity() { // we don't know the length public long getContentLength() { return -1; } public String getContentType() { return ClientUtils.TEXT_XML; } public boolean isRepeatable() { return false; } public void writeRequest(OutputStream out) throws IOException { try { OutputStreamWriter writer = new OutputStreamWriter(out, "UTF-8"); writer.append( "<stream>" ); // can be anything... UpdateRequest req = queue.poll( 250, TimeUnit.MILLISECONDS ); while( req != null ) { log.debug( "sending: {}" , req ); req.writeXML( writer ); // check for commit or optimize SolrParams params = req.getParams(); if( params != null ) { String fmt = null; if( params.getBool( UpdateParams.OPTIMIZE, false ) ) { fmt = "<optimize waitSearcher=\"%s\" waitFlush=\"%s\" />"; } else if( params.getBool( UpdateParams.COMMIT, false ) ) { fmt = "<commit waitSearcher=\"%s\" waitFlush=\"%s\" />"; } if( fmt != null ) { log.info( fmt ); writer.write( String.format( fmt, params.getBool( UpdateParams.WAIT_SEARCHER, false )+"", params.getBool( UpdateParams.WAIT_FLUSH, false )+"") ); } } writer.flush(); req = queue.poll( 250, TimeUnit.MILLISECONDS ); } writer.append( "</stream>" ); writer.flush(); } catch (InterruptedException e) { e.printStackTrace(); } } }; method = new PostMethod(_baseURL+updateUrl ); method.setRequestEntity( request ); method.setFollowRedirects( false ); method.addRequestHeader( "User-Agent", AGENT ); int statusCode = getHttpClient().executeMethod(method); if (statusCode != HttpStatus.SC_OK) { StringBuilder msg = new StringBuilder(); msg.append( method.getStatusLine().getReasonPhrase() ); msg.append( "\n\n" ); msg.append( method.getStatusText() ); msg.append( "\n\n" ); msg.append( "request: "+method.getURI() ); handleError( new Exception( msg.toString() ) ); } } finally { try { // make sure to release the connection if(method != null) method.releaseConnection(); } catch( Exception ex ){} } } while( ! queue.isEmpty()); } catch (Throwable e) { handleError( e ); } finally { // remove it from the list of running things... synchronized (runners) { runners.remove( this ); } log.info( "finished: {}" , this ); runnerLock.unlock(); } } } @Override public NamedList<Object> request( final SolrRequest request ) throws SolrServerException, IOException { if( !(request instanceof UpdateRequest) ) { return super.request( request ); } UpdateRequest req = (UpdateRequest)request; // this happens for commit... if( req.getDocuments()==null || req.getDocuments().isEmpty() ) { blockUntilFinished(); return super.request( request ); } SolrParams params = req.getParams(); if( params != null ) { // check if it is waiting for the searcher if( params.getBool( UpdateParams.WAIT_SEARCHER, false ) ) { log.info( "blocking for commit/optimize" ); blockUntilFinished(); // empty the queue return super.request( request ); } } try { CountDownLatch tmpLock = lock; if( tmpLock != null ) { tmpLock.await(); } queue.put( req ); synchronized( runners ) { if( runners.isEmpty() || (queue.remainingCapacity() < queue.size() && runners.size() < threadCount) ) { Runner r = new Runner(); scheduler.execute( r ); runners.add( r ); } } } catch (InterruptedException e) { log.error( "interrupted", e ); throw new IOException( e.getLocalizedMessage() ); } // RETURN A DUMMY result NamedList<Object> dummy = new NamedList<Object>(); dummy.add( "NOTE", "the request is processed in a background stream" ); return dummy; } public synchronized void blockUntilFinished() { lock = new CountDownLatch(1); try { // Wait until no runners are running Runner runner = runners.peek(); while( runner != null ) { runner.runnerLock.lock(); runner.runnerLock.unlock(); runner = runners.peek(); } } finally { lock.countDown(); lock=null; } } public void handleError( Throwable ex ) { log.error( "error", ex ); } }