/*
* Hibernate Search, full-text search for your domain model
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.search.elasticsearch.work.impl;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import org.elasticsearch.client.Response;
import org.hibernate.search.backend.LuceneWork;
import org.hibernate.search.elasticsearch.client.impl.ElasticsearchRequest;
import org.hibernate.search.elasticsearch.client.impl.Paths;
import org.hibernate.search.elasticsearch.client.impl.URLEncodedString;
import org.hibernate.search.elasticsearch.gson.impl.GsonProvider;
import org.hibernate.search.elasticsearch.logging.impl.Log;
import org.hibernate.search.elasticsearch.util.impl.ElasticsearchClientUtils;
import org.hibernate.search.elasticsearch.work.impl.builder.BulkWorkBuilder;
import org.hibernate.search.exception.SearchException;
import org.hibernate.search.util.impl.CollectionHelper;
import org.hibernate.search.util.logging.impl.LoggerFactory;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
/**
* @author Yoann Rodiere
*/
public class BulkWork implements ElasticsearchWork<Void> {
private static final Log LOG = LoggerFactory.make( Log.class );
private final ElasticsearchRequest request;
private final List<BulkableElasticsearchWork<?>> works;
/**
* Whether to perform a refresh in the course of executing this bulk or not.
* <p>
* Note that this will refresh all indexes touched by this bulk,
* not only those given via {@link #indexesNeedingRefresh}. That's acceptable.
* <p>
* If {@code true}, no additional refresh of the concerned indexes
* is needed after executing the bulk.
*/
private final boolean refreshInAPICall;
protected BulkWork(Builder builder) {
super();
this.request = builder.buildRequest();
this.works = new ArrayList<>( builder.bulkableWorks );
this.refreshInAPICall = builder.refreshInBulkAPICall;
}
@Override
public String toString() {
return new StringBuilder()
.append( getClass().getSimpleName() )
.append( "[" )
.append( "works = " ).append( works )
.append( ", refreshInAPICall = " ).append( refreshInAPICall )
.append( "]" )
.toString();
}
@Override
public Void execute(ElasticsearchWorkExecutionContext context) {
if ( refreshInAPICall ) {
/*
* Prevent bulked works to mark indexes as dirty,
* since we refresh all indexes as part of the Bulk API call.
*/
context = new NoIndexDirtyBulkExecutionContext( context );
}
GsonProvider gsonProvider = context.getGsonProvider();
Response response = null;
JsonObject parsedResponseBody = null;
try {
response = context.getClient().execute( request );
parsedResponseBody = ElasticsearchClientUtils.parseJsonResponse( gsonProvider, response );
handleResults( context, response, parsedResponseBody );
return null;
}
catch (SearchException e) {
throw e; // Do not add context for those: we expect SearchExceptions to be self-explanatory
}
catch (IOException | RuntimeException e) {
throw LOG.elasticsearchRequestFailed(
ElasticsearchClientUtils.formatRequest( gsonProvider, request ),
ElasticsearchClientUtils.formatResponse( gsonProvider, response, parsedResponseBody ),
e );
}
}
@Override
public void aggregate(ElasticsearchWorkAggregator aggregator) {
aggregator.addNonBulkable( this );
}
@Override
public Stream<LuceneWork> getLuceneWorks() {
Stream<LuceneWork> result = Stream.empty();
for ( BulkableElasticsearchWork<?> work : works ) {
result = Stream.concat( result, work.getLuceneWorks() );
}
return result;
}
/*
* Give the chance for every work to handle the result,
* making sure that exceptions are handled properly
* so that one failing handler will not prevent others from being called.
*
* If at least one work or its result handler failed,
* an exception will be thrown after every result has been handled.
*/
private void handleResults(ElasticsearchWorkExecutionContext context, Response response, JsonObject parsedResponseBody) {
Map<BulkableElasticsearchWork<?>, JsonObject> successfulItems =
CollectionHelper.newHashMap( works.size() );
List<BulkableElasticsearchWork<?>> erroneousItems = new ArrayList<>();
int i = 0;
JsonArray resultItems = parsedResponseBody.has( "items" ) ? parsedResponseBody.get( "items" ).getAsJsonArray() : null;
List<RuntimeException> resultHandlingExceptions = null;
for ( BulkableElasticsearchWork<?> work : works ) {
JsonObject resultItem = resultItems != null ? resultItems.get( i ).getAsJsonObject() : null;
boolean success;
try {
success = work.handleBulkResult( context, resultItem );
}
catch (RuntimeException e) {
if ( resultHandlingExceptions == null ) {
resultHandlingExceptions = new ArrayList<>();
}
resultHandlingExceptions.add( e );
success = false;
}
if ( success ) {
successfulItems.put( work, resultItem );
}
else {
erroneousItems.add( work );
}
++i;
}
if ( !erroneousItems.isEmpty() ) {
GsonProvider gsonProvider = context.getGsonProvider();
BulkRequestFailedException exception = LOG.elasticsearchBulkRequestFailed(
ElasticsearchClientUtils.formatRequest( gsonProvider, request ),
ElasticsearchClientUtils.formatResponse( gsonProvider, response, parsedResponseBody ),
successfulItems,
erroneousItems
);
if ( resultHandlingExceptions != null ) {
for ( Exception resultHandlingException : resultHandlingExceptions ) {
exception.addSuppressed( resultHandlingException );
}
}
throw exception;
}
}
private static class NoIndexDirtyBulkExecutionContext extends ForwardingElasticsearchWorkExecutionContext {
public NoIndexDirtyBulkExecutionContext(ElasticsearchWorkExecutionContext delegate) {
super( delegate );
}
@Override
public void setIndexDirty(URLEncodedString indexName) {
// Don't delegate
}
}
public static class Builder implements BulkWorkBuilder {
private final List<BulkableElasticsearchWork<?>> bulkableWorks;
private boolean refreshInBulkAPICall;
public Builder(List<BulkableElasticsearchWork<?>> bulkableWorks) {
this.bulkableWorks = bulkableWorks;
}
@Override
public Builder refresh(boolean refresh) {
this.refreshInBulkAPICall = refresh;
return this;
}
protected ElasticsearchRequest buildRequest() {
ElasticsearchRequest.Builder builder =
ElasticsearchRequest.post()
.pathComponent( Paths._BULK )
.param( "refresh", refreshInBulkAPICall );
for ( BulkableElasticsearchWork<?> work : bulkableWorks ) {
builder.body( work.getBulkableActionMetadata() );
JsonObject actionBody = work.getBulkableActionBody();
if ( actionBody != null ) {
builder.body( actionBody );
}
}
return builder.build();
}
@Override
public BulkWork build() {
return new BulkWork( this );
}
}
}