diff --git a/documentation/src/main/asciidoc/mapper-orm-indexing-massindexer.asciidoc b/documentation/src/main/asciidoc/mapper-orm-indexing-massindexer.asciidoc index ed5f88b815a..a94927d2460 100644 --- a/documentation/src/main/asciidoc/mapper-orm-indexing-massindexer.asciidoc +++ b/documentation/src/main/asciidoc/mapper-orm-indexing-massindexer.asciidoc @@ -193,6 +193,25 @@ and passing an instance using the `monitor` method. + Implementations of `MassIndexingMonitor` must be threadsafe. +|`failureHandler(MassIndexingFailureHandler)` +|A failure handler. +| +The component responsible for handling failures occurring during mass indexing. ++ +A `MassIndexer` performs multiple operations in parallel, +some of which can fail without stopping the whole mass indexing process. +As a result, it may be necessary to trace individual failures. +The default, built-in failure handler just forwards the failures +to the global <>, +which by default will log them at the `ERROR` level, +but a custom handler can be set by implementing the `MassIndexingFailureHandler` interface +and passing an instance using the `failureHandler` method. +This can be used to simply log failures in a context specific to the mass indexer, +e.g. a web interface in a maintenance console from which mass indexing was requested, +or for more advanced use cases, such as cancelling mass indexing on the first failure. ++ +Implementations of `MassIndexingFailureHandler` must be threadsafe. + |=== [[mapper-orm-indexing-massindexer-tuning]] diff --git a/engine/src/main/java/org/hibernate/search/engine/reporting/FailureHandler.java b/engine/src/main/java/org/hibernate/search/engine/reporting/FailureHandler.java index 7e0143b2a9b..9f50002f3ce 100644 --- a/engine/src/main/java/org/hibernate/search/engine/reporting/FailureHandler.java +++ b/engine/src/main/java/org/hibernate/search/engine/reporting/FailureHandler.java @@ -15,9 +15,6 @@ * but it can be replaced with a custom implementations through * {@link org.hibernate.search.engine.cfg.EngineSettings#BACKGROUND_FAILURE_HANDLER a configuration property}. *

- * Handlers should never throw any exception: - * doing so will lead to undetermined behavior in Hibernate Search background threads. - *

* Handlers can be called from multiple threads simultaneously: implementations must be thread-safe. * * @author Amin Mohammed-Coleman @@ -31,8 +28,7 @@ public interface FailureHandler { * then return as quickly as possible. * Heavy error processing (sending emails, ...), if any, should be done asynchronously. *

- * This method should never throw any exception: - * doing so will lead to undetermined behavior in Hibernate Search background threads. + * Any error or exception thrown by this method will be caught by Hibernate Search and logged. * * @param context Contextual information about the failure (throwable, operation, ...) */ @@ -45,8 +41,7 @@ public interface FailureHandler { * then return as quickly as possible. * Heavy error processing (sending emails, ...), if any, should be done asynchronously. *

- * This method should never throw any exception: - * doing so will lead to undetermined behavior in Hibernate Search background threads. + * Any error or exception thrown by this method will be caught by Hibernate Search and logged. * * @param context Contextual information about the failure (throwable, operation, ...) */ @@ -59,8 +54,7 @@ public interface FailureHandler { * then return as quickly as possible. * Heavy error processing (sending emails, ...), if any, should be done asynchronously. *

- * This method should never throw any exception: - * doing so will lead to undetermined behavior in Hibernate Search background threads. + * Any error or exception thrown by this method will be caught by Hibernate Search and logged. * * @param context Contextual information about the failure (throwable, operation, ...) */ diff --git a/integrationtest/mapper/orm/src/test/java/org/hibernate/search/integrationtest/mapper/orm/massindexing/AbstractMassIndexingFailureIT.java b/integrationtest/mapper/orm/src/test/java/org/hibernate/search/integrationtest/mapper/orm/massindexing/AbstractMassIndexingFailureIT.java new file mode 100644 index 00000000000..e8b4779737a --- /dev/null +++ b/integrationtest/mapper/orm/src/test/java/org/hibernate/search/integrationtest/mapper/orm/massindexing/AbstractMassIndexingFailureIT.java @@ -0,0 +1,613 @@ +/* + * 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 . + */ +package org.hibernate.search.integrationtest.mapper.orm.massindexing; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Fail.fail; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import javax.persistence.Entity; +import javax.persistence.Id; + +import org.hibernate.SessionFactory; +import org.hibernate.search.engine.backend.work.execution.DocumentCommitStrategy; +import org.hibernate.search.engine.backend.work.execution.DocumentRefreshStrategy; +import org.hibernate.search.engine.cfg.EngineSettings; +import org.hibernate.search.engine.cfg.spi.EngineSpiSettings; +import org.hibernate.search.mapper.orm.Search; +import org.hibernate.search.mapper.orm.automaticindexing.AutomaticIndexingStrategyName; +import org.hibernate.search.mapper.orm.cfg.HibernateOrmMapperSettings; +import org.hibernate.search.mapper.orm.massindexing.MassIndexer; +import org.hibernate.search.mapper.orm.massindexing.MassIndexingFailureHandler; +import org.hibernate.search.mapper.pojo.mapping.definition.annotation.GenericField; +import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed; +import org.hibernate.search.util.common.SearchException; +import org.hibernate.search.util.impl.integrationtest.common.rule.BackendMock; +import org.hibernate.search.util.impl.integrationtest.common.rule.ThreadSpy; +import org.hibernate.search.util.impl.integrationtest.common.stub.backend.index.StubIndexScopeWork; +import org.hibernate.search.util.impl.integrationtest.mapper.orm.OrmSetupHelper; +import org.hibernate.search.util.impl.integrationtest.mapper.orm.OrmUtils; +import org.hibernate.search.util.impl.test.SubTest; + +import org.junit.Rule; +import org.junit.Test; + +import org.assertj.core.api.InstanceOfAssertFactories; +import org.awaitility.Awaitility; + +public abstract class AbstractMassIndexingFailureIT { + + public static final String TITLE_1 = "Oliver Twist"; + public static final String AUTHOR_1 = "Charles Dickens"; + public static final String TITLE_2 = "Ulysses"; + public static final String AUTHOR_2 = "James Joyce"; + public static final String TITLE_3 = "Frankenstein"; + public static final String AUTHOR_3 = "Mary Shelley"; + + @Rule + public BackendMock backendMock = new BackendMock( "stubBackend" ); + + @Rule + public OrmSetupHelper ormSetupHelper = OrmSetupHelper.withBackendMock( backendMock ); + + @Rule + public ThreadSpy threadSpy = new ThreadSpy(); + + @Test + public void indexing() { + SessionFactory sessionFactory = setup(); + + String entityName = Book.NAME; + String entityReferenceAsString = Book.NAME + "#2"; + String exceptionMessage = "Indexing failure"; + String failingOperationAsString = "Indexing instance of entity '" + entityName + "' during mass indexing"; + + expectEntityIndexingFailureHandling( + entityName, entityReferenceAsString, + exceptionMessage, failingOperationAsString + ); + + doMassIndexingWithFailure( + Search.mapping( sessionFactory ).scope( Object.class ).massIndexer(), + ThreadExpectation.CREATED_AND_TERMINATED, + throwable -> assertThat( throwable ).isInstanceOf( SearchException.class ) + .hasMessageContainingAll( + "1 entities could not be indexed", + "See the logs for details.", + "First failure on entity 'Book#2': ", + exceptionMessage + ) + .hasCauseInstanceOf( SimulatedFailure.class ), + expectIndexScopeWork( StubIndexScopeWork.Type.PURGE, ExecutionExpectation.SUCCEED ), + expectIndexScopeWork( StubIndexScopeWork.Type.MERGE_SEGMENTS, ExecutionExpectation.SUCCEED ), + expectIndexingWorks( ExecutionExpectation.FAIL ), + expectIndexScopeWork( StubIndexScopeWork.Type.FLUSH, ExecutionExpectation.SUCCEED ) + ); + + assertEntityIndexingFailureHandling( + entityName, entityReferenceAsString, + exceptionMessage, failingOperationAsString + ); + } + + @Test + public void getId() { + SessionFactory sessionFactory = setup(); + + String entityName = Book.NAME; + String entityReferenceAsString = Book.NAME + "#2"; + String exceptionMessage = "getId failure"; + String failingOperationAsString = "Indexing instance of entity '" + entityName + "' during mass indexing"; + + expectEntityGetterFailureHandling( + entityName, entityReferenceAsString, + exceptionMessage, failingOperationAsString + ); + + doMassIndexingWithFailure( + Search.mapping( sessionFactory ).scope( Object.class ).massIndexer(), + ThreadExpectation.CREATED_AND_TERMINATED, + throwable -> assertThat( throwable ).isInstanceOf( SearchException.class ) + .hasMessageContainingAll( + "1 entities could not be indexed", + "See the logs for details.", + "First failure on entity 'Book#2': ", + "Exception while invoking" + ) + .extracting( Throwable::getCause ).asInstanceOf( InstanceOfAssertFactories.THROWABLE ) + .isInstanceOf( SearchException.class ) + .hasMessageContaining( "Exception while invoking" ), + ExecutionExpectation.FAIL, ExecutionExpectation.SKIP, + expectIndexScopeWork( StubIndexScopeWork.Type.PURGE, ExecutionExpectation.SUCCEED ), + expectIndexScopeWork( StubIndexScopeWork.Type.MERGE_SEGMENTS, ExecutionExpectation.SUCCEED ), + expectIndexingWorks( ExecutionExpectation.SKIP ), + expectIndexScopeWork( StubIndexScopeWork.Type.FLUSH, ExecutionExpectation.SUCCEED ) + ); + + assertEntityGetterFailureHandling( + entityName, entityReferenceAsString, + exceptionMessage, failingOperationAsString + ); + } + + @Test + public void getTitle() { + SessionFactory sessionFactory = setup(); + + String entityName = Book.NAME; + String entityReferenceAsString = Book.NAME + "#2"; + String exceptionMessage = "getTitle failure"; + String failingOperationAsString = "Indexing instance of entity '" + entityName + "' during mass indexing"; + + expectEntityGetterFailureHandling( + entityName, entityReferenceAsString, + exceptionMessage, failingOperationAsString + ); + + doMassIndexingWithFailure( + Search.mapping( sessionFactory ).scope( Object.class ).massIndexer(), + ThreadExpectation.CREATED_AND_TERMINATED, + throwable -> assertThat( throwable ).isInstanceOf( SearchException.class ) + .hasMessageContainingAll( + "1 entities could not be indexed", + "See the logs for details.", + "First failure on entity 'Book#2': ", + "Exception while invoking" + ) + .extracting( Throwable::getCause ).asInstanceOf( InstanceOfAssertFactories.THROWABLE ) + .isInstanceOf( SearchException.class ) + .hasMessageContaining( "Exception while invoking" ), + ExecutionExpectation.SUCCEED, ExecutionExpectation.FAIL, + expectIndexScopeWork( StubIndexScopeWork.Type.PURGE, ExecutionExpectation.SUCCEED ), + expectIndexScopeWork( StubIndexScopeWork.Type.MERGE_SEGMENTS, ExecutionExpectation.SUCCEED ), + expectIndexingWorks( ExecutionExpectation.SKIP ), + expectIndexScopeWork( StubIndexScopeWork.Type.FLUSH, ExecutionExpectation.SUCCEED ) + ); + + assertEntityGetterFailureHandling( + entityName, entityReferenceAsString, + exceptionMessage, failingOperationAsString + ); + } + + @Test + public void purge() { + SessionFactory sessionFactory = setup(); + + String exceptionMessage = "PURGE failure"; + String failingOperationAsString = "MassIndexer operation"; + + expectMassIndexerOperationFailureHandling( exceptionMessage, failingOperationAsString ); + + doMassIndexingWithFailure( + Search.mapping( sessionFactory ).scope( Object.class ).massIndexer(), + ThreadExpectation.NOT_CREATED, + throwable -> assertThat( throwable ).isInstanceOf( SimulatedFailure.class ) + .hasMessageContaining( exceptionMessage ), + expectIndexScopeWork( StubIndexScopeWork.Type.PURGE, ExecutionExpectation.FAIL ) + ); + + assertMassIndexerOperationFailureHandling( exceptionMessage, failingOperationAsString ); + } + + @Test + public void mergeSegmentsBefore() { + SessionFactory sessionFactory = setup(); + + String exceptionMessage = "MERGE_SEGMENTS failure"; + String failingOperationAsString = "MassIndexer operation"; + + expectMassIndexerOperationFailureHandling( exceptionMessage, failingOperationAsString ); + + doMassIndexingWithFailure( + Search.mapping( sessionFactory ).scope( Object.class ).massIndexer(), + ThreadExpectation.NOT_CREATED, + throwable -> assertThat( throwable ).isInstanceOf( SimulatedFailure.class ) + .hasMessageContaining( exceptionMessage ), + expectIndexScopeWork( StubIndexScopeWork.Type.PURGE, ExecutionExpectation.SUCCEED ), + expectIndexScopeWork( StubIndexScopeWork.Type.MERGE_SEGMENTS, ExecutionExpectation.FAIL ) + ); + + assertMassIndexerOperationFailureHandling( exceptionMessage, failingOperationAsString ); + } + + @Test + public void mergeSegmentsAfter() { + SessionFactory sessionFactory = setup(); + + String exceptionMessage = "MERGE_SEGMENTS failure"; + String failingOperationAsString = "MassIndexer operation"; + + expectMassIndexerOperationFailureHandling( exceptionMessage, failingOperationAsString ); + + doMassIndexingWithFailure( + Search.mapping( sessionFactory ).scope( Object.class ).massIndexer() + .mergeSegmentsOnFinish( true ), + ThreadExpectation.CREATED_AND_TERMINATED, + throwable -> assertThat( throwable ).isInstanceOf( SimulatedFailure.class ) + .hasMessageContaining( exceptionMessage ), + expectIndexScopeWork( StubIndexScopeWork.Type.PURGE, ExecutionExpectation.SUCCEED ), + expectIndexScopeWork( StubIndexScopeWork.Type.MERGE_SEGMENTS, ExecutionExpectation.SUCCEED ), + expectIndexingWorks( ExecutionExpectation.SUCCEED ), + expectIndexScopeWork( StubIndexScopeWork.Type.MERGE_SEGMENTS, ExecutionExpectation.FAIL ) + ); + + assertMassIndexerOperationFailureHandling( exceptionMessage, failingOperationAsString ); + } + + @Test + public void flush() { + SessionFactory sessionFactory = setup(); + + String exceptionMessage = "FLUSH failure"; + String failingOperationAsString = "MassIndexer operation"; + + expectMassIndexerOperationFailureHandling( exceptionMessage, failingOperationAsString ); + + doMassIndexingWithFailure( + Search.mapping( sessionFactory ).scope( Object.class ).massIndexer(), + ThreadExpectation.CREATED_AND_TERMINATED, + throwable -> assertThat( throwable ).isInstanceOf( SimulatedFailure.class ) + .hasMessageContaining( exceptionMessage ), + expectIndexScopeWork( StubIndexScopeWork.Type.PURGE, ExecutionExpectation.SUCCEED ), + expectIndexScopeWork( StubIndexScopeWork.Type.MERGE_SEGMENTS, ExecutionExpectation.SUCCEED ), + expectIndexingWorks( ExecutionExpectation.SUCCEED ), + expectIndexScopeWork( StubIndexScopeWork.Type.FLUSH, ExecutionExpectation.FAIL ) + ); + + assertMassIndexerOperationFailureHandling( exceptionMessage, failingOperationAsString ); + } + + @Test + public void indexingAndFlush() { + SessionFactory sessionFactory = setup(); + + String entityName = Book.NAME; + String entityReferenceAsString = Book.NAME + "#2"; + String failingEntityIndexingExceptionMessage = "Indexing failure"; + String failingEntityIndexingOperationAsString = "Indexing instance of entity '" + entityName + "' during mass indexing"; + String failingMassIndexerOperationExceptionMessage = "FLUSH failure"; + String failingMassIndexerOperationAsString = "MassIndexer operation"; + + expectEntityIndexingAndMassIndexerOperationFailureHandling( + entityName, entityReferenceAsString, + failingEntityIndexingExceptionMessage, failingEntityIndexingOperationAsString, + failingMassIndexerOperationExceptionMessage, failingMassIndexerOperationAsString + ); + + doMassIndexingWithFailure( + Search.mapping( sessionFactory ).scope( Object.class ).massIndexer(), + ThreadExpectation.CREATED_AND_TERMINATED, + throwable -> assertThat( throwable ).isInstanceOf( SimulatedFailure.class ) + .hasMessageContaining( failingMassIndexerOperationExceptionMessage ) + // Indexing failure should also be mentioned as a suppressed exception + .extracting( Throwable::getSuppressed ).asInstanceOf( InstanceOfAssertFactories.ARRAY ) + .anySatisfy( suppressed -> assertThat( suppressed ).asInstanceOf( InstanceOfAssertFactories.THROWABLE ) + .isInstanceOf( SearchException.class ) + .hasMessageContainingAll( + "1 entities could not be indexed", + "See the logs for details.", + "First failure on entity 'Book#2': ", + failingEntityIndexingExceptionMessage + ) + .hasCauseInstanceOf( SimulatedFailure.class ) + ), + expectIndexScopeWork( StubIndexScopeWork.Type.PURGE, ExecutionExpectation.SUCCEED ), + expectIndexScopeWork( StubIndexScopeWork.Type.MERGE_SEGMENTS, ExecutionExpectation.SUCCEED ), + expectIndexingWorks( ExecutionExpectation.FAIL ), + expectIndexScopeWork( StubIndexScopeWork.Type.FLUSH, ExecutionExpectation.FAIL ) + ); + + assertEntityIndexingAndMassIndexerOperationFailureHandling( + entityName, entityReferenceAsString, + failingEntityIndexingExceptionMessage, failingEntityIndexingOperationAsString, + failingMassIndexerOperationExceptionMessage, failingMassIndexerOperationAsString + ); + } + + protected abstract String getBackgroundFailureHandlerReference(); + + protected abstract MassIndexingFailureHandler getMassIndexingFailureHandler(); + + protected void assertBeforeSetup() { + } + + protected void assertAfterSetup() { + } + + protected abstract void expectEntityIndexingFailureHandling(String entityName, String entityReferenceAsString, + String exceptionMessage, String failingOperationAsString); + + protected abstract void assertEntityIndexingFailureHandling(String entityName, String entityReferenceAsString, + String exceptionMessage, String failingOperationAsString); + + protected abstract void expectEntityGetterFailureHandling(String entityName, String entityReferenceAsString, + String exceptionMessage, String failingOperationAsString); + + protected abstract void assertEntityGetterFailureHandling(String entityName, String entityReferenceAsString, + String exceptionMessage, String failingOperationAsString); + + protected abstract void expectMassIndexerOperationFailureHandling( + String exceptionMessage, String failingOperationAsString); + + protected abstract void assertMassIndexerOperationFailureHandling( + String exceptionMessage, String failingOperationAsString); + + protected abstract void expectEntityIndexingAndMassIndexerOperationFailureHandling( + String entityName, String entityReferenceAsString, + String failingEntityIndexingExceptionMessage, String failingEntityIndexingOperationAsString, + String failingMassIndexerOperationExceptionMessage, String failingMassIndexerOperationAsString); + + protected abstract void assertEntityIndexingAndMassIndexerOperationFailureHandling( + String entityName, String entityReferenceAsString, + String failingEntityIndexingExceptionMessage, String failingEntityIndexingOperationAsString, + String failingMassIndexerOperationExceptionMessage, String failingMassIndexerOperationAsString); + + private void doMassIndexingWithFailure(MassIndexer massIndexer, + ThreadExpectation threadExpectation, + Consumer thrownExpectation, + Runnable ... expectationSetters) { + doMassIndexingWithFailure( + massIndexer, + threadExpectation, + thrownExpectation, + ExecutionExpectation.SUCCEED, ExecutionExpectation.SUCCEED, + expectationSetters + ); + } + + private void doMassIndexingWithFailure(MassIndexer massIndexer, + ThreadExpectation threadExpectation, + Consumer thrownExpectation, + ExecutionExpectation book2GetIdExpectation, ExecutionExpectation book2GetTitleExpectation, + Runnable ... expectationSetters) { + Book.failOnBook2GetId.set( ExecutionExpectation.FAIL.equals( book2GetIdExpectation ) ); + Book.failOnBook2GetTitle.set( ExecutionExpectation.FAIL.equals( book2GetTitleExpectation ) ); + AssertionError assertionError = null; + try { + MassIndexingFailureHandler massIndexingFailureHandler = getMassIndexingFailureHandler(); + if ( massIndexingFailureHandler != null ) { + massIndexer.failureHandler( massIndexingFailureHandler ); + } + + for ( Runnable expectationSetter : expectationSetters ) { + expectationSetter.run(); + } + + // TODO HSEARCH-3728 simplify this when even indexing exceptions are propagated + Runnable runnable = () -> { + try { + massIndexer.startAndWait(); + } + catch (InterruptedException e) { + fail( "Unexpected InterruptedException: " + e.getMessage() ); + } + }; + if ( thrownExpectation == null ) { + runnable.run(); + } + else { + SubTest.expectException( runnable ) + .assertThrown() + .satisfies( thrownExpectation ); + } + backendMock.verifyExpectationsMet(); + } + catch (AssertionError e) { + assertionError = e; + throw e; + } + finally { + Book.failOnBook2GetId.set( false ); + Book.failOnBook2GetTitle.set( false ); + + if ( assertionError == null ) { + switch ( threadExpectation ) { + case CREATED_AND_TERMINATED: + Awaitility.await().untilAsserted( + () -> assertThat( threadSpy.getCreatedThreads( "mass index" ) ) + .as( "Mass indexing threads" ) + .isNotEmpty() + .allSatisfy( t -> assertThat( t ) + .extracting( Thread::getState ) + .isEqualTo( Thread.State.TERMINATED ) + ) + ); + break; + case NOT_CREATED: + assertThat( threadSpy.getCreatedThreads( "mass index" ) ) + .as( "Mass indexing threads" ) + .isEmpty(); + break; + } + } + } + } + + private Runnable expectIndexScopeWork(StubIndexScopeWork.Type type, ExecutionExpectation executionExpectation) { + return () -> { + switch ( executionExpectation ) { + case SUCCEED: + backendMock.expectIndexScopeWorks( Book.NAME ) + .indexScopeWork( type ); + break; + case FAIL: + CompletableFuture failingFuture = new CompletableFuture<>(); + failingFuture.completeExceptionally( new SimulatedFailure( type.name() + " failure" ) ); + backendMock.expectIndexScopeWorks( Book.NAME ) + .indexScopeWork( type, failingFuture ); + break; + case SKIP: + break; + } + }; + } + + private Runnable expectIndexingWorks(ExecutionExpectation workTwoExecutionExpectation) { + return () -> { + switch ( workTwoExecutionExpectation ) { + case SUCCEED: + backendMock.expectWorksAnyOrder( + Book.NAME, DocumentCommitStrategy.NONE, DocumentRefreshStrategy.NONE + ) + .add( "1", b -> b + .field( "title", TITLE_1 ) + .field( "author", AUTHOR_1 ) + ) + .add( "2", b -> b + .field( "title", TITLE_2 ) + .field( "author", AUTHOR_2 ) + ) + .add( "3", b -> b + .field( "title", TITLE_3 ) + .field( "author", AUTHOR_3 ) + ) + .processedThenExecuted(); + break; + case FAIL: + CompletableFuture failingFuture = new CompletableFuture<>(); + failingFuture.completeExceptionally( new SimulatedFailure( "Indexing failure" ) ); + backendMock.expectWorksAnyOrder( + Book.NAME, DocumentCommitStrategy.NONE, DocumentRefreshStrategy.NONE + ) + .add( "1", b -> b + .field( "title", TITLE_1 ) + .field( "author", AUTHOR_1 ) + ) + .add( "3", b -> b + .field( "title", TITLE_3 ) + .field( "author", AUTHOR_3 ) + ) + .processedThenExecuted(); + backendMock.expectWorksAnyOrder( + Book.NAME, DocumentCommitStrategy.NONE, DocumentRefreshStrategy.NONE + ) + .add( "2", b -> b + .field( "title", TITLE_2 ) + .field( "author", AUTHOR_2 ) + ) + .processedThenExecuted( failingFuture ); + break; + case SKIP: + backendMock.expectWorksAnyOrder( + Book.NAME, DocumentCommitStrategy.NONE, DocumentRefreshStrategy.NONE + ) + .add( "1", b -> b + .field( "title", TITLE_1 ) + .field( "author", AUTHOR_1 ) + ) + .add( "3", b -> b + .field( "title", TITLE_3 ) + .field( "author", AUTHOR_3 ) + ) + .processedThenExecuted(); + break; + } + }; + } + + private SessionFactory setup() { + assertBeforeSetup(); + + backendMock.expectAnySchema( Book.NAME ); + + SessionFactory sessionFactory = ormSetupHelper.start() + .withPropertyRadical( HibernateOrmMapperSettings.Radicals.AUTOMATIC_INDEXING_STRATEGY, AutomaticIndexingStrategyName.NONE ) + .withPropertyRadical( EngineSettings.Radicals.BACKGROUND_FAILURE_HANDLER, getBackgroundFailureHandlerReference() ) + .withPropertyRadical( EngineSpiSettings.Radicals.THREAD_PROVIDER, threadSpy.getThreadProvider() ) + .setup( Book.class ); + + backendMock.verifyExpectationsMet(); + + OrmUtils.withinTransaction( sessionFactory, session -> { + session.persist( new Book( 1, TITLE_1, AUTHOR_1 ) ); + session.persist( new Book( 2, TITLE_2, AUTHOR_2 ) ); + session.persist( new Book( 3, TITLE_3, AUTHOR_3 ) ); + } ); + + assertAfterSetup(); + + return sessionFactory; + } + + private enum ExecutionExpectation { + SUCCEED, + FAIL, + SKIP; + } + + private enum ThreadExpectation { + CREATED_AND_TERMINATED, + NOT_CREATED; + } + + @Entity(name = Book.NAME) + @Indexed(index = Book.NAME) + public static class Book { + + public static final String NAME = "Book"; + + private static final AtomicBoolean failOnBook2GetId = new AtomicBoolean( false ); + private static final AtomicBoolean failOnBook2GetTitle = new AtomicBoolean( false ); + + private Integer id; + + private String title; + + private String author; + + public Book() { + } + + public Book(Integer id, String title, String author) { + this.id = id; + this.title = title; + this.author = author; + } + + @Id // This must be on the getter, so that Hibernate Search uses getters instead of direct field access + public Integer getId() { + if ( id == 2 && failOnBook2GetId.get() ) { + throw new SimulatedFailure( "getId failure" ); + } + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + @GenericField + public String getTitle() { + if ( id == 2 && failOnBook2GetTitle.get() ) { + throw new SimulatedFailure( "getTitle failure" ); + } + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + @GenericField + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + } + + protected static class SimulatedFailure extends RuntimeException { + SimulatedFailure(String message) { + super( message ); + } + } +} diff --git a/integrationtest/mapper/orm/src/test/java/org/hibernate/search/integrationtest/mapper/orm/massindexing/MassIndexingFailureCustomBackgroundFailureHandlerIT.java b/integrationtest/mapper/orm/src/test/java/org/hibernate/search/integrationtest/mapper/orm/massindexing/MassIndexingFailureCustomBackgroundFailureHandlerIT.java new file mode 100644 index 00000000000..3e41ab6fb59 --- /dev/null +++ b/integrationtest/mapper/orm/src/test/java/org/hibernate/search/integrationtest/mapper/orm/massindexing/MassIndexingFailureCustomBackgroundFailureHandlerIT.java @@ -0,0 +1,108 @@ +/* + * 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 . + */ +package org.hibernate.search.integrationtest.mapper.orm.massindexing; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.hibernate.search.mapper.orm.massindexing.MassIndexingFailureHandler; +import org.hibernate.search.util.impl.integrationtest.common.stub.StubFailureHandler; +import org.hibernate.search.util.impl.test.rule.StaticCounters; + +import org.junit.Rule; + +public class MassIndexingFailureCustomBackgroundFailureHandlerIT extends AbstractMassIndexingFailureIT { + + @Rule + public StaticCounters staticCounters = new StaticCounters(); + + @Override + protected String getBackgroundFailureHandlerReference() { + return StubFailureHandler.class.getName(); + } + + @Override + protected MassIndexingFailureHandler getMassIndexingFailureHandler() { + return null; + } + + @Override + protected void assertBeforeSetup() { + assertThat( staticCounters.get( StubFailureHandler.CREATE ) ).isEqualTo( 0 ); + assertThat( staticCounters.get( StubFailureHandler.HANDLE_INDEX_CONTEXT ) ).isEqualTo( 0 ); + assertThat( staticCounters.get( StubFailureHandler.HANDLE_GENERIC_CONTEXT ) ).isEqualTo( 0 ); + assertThat( staticCounters.get( StubFailureHandler.HANDLE_ENTITY_INDEXING_CONTEXT ) ).isEqualTo( 0 ); + } + + @Override + protected void assertAfterSetup() { + assertThat( staticCounters.get( StubFailureHandler.CREATE ) ).isEqualTo( 1 ); + assertThat( staticCounters.get( StubFailureHandler.HANDLE_INDEX_CONTEXT ) ).isEqualTo( 0 ); + assertThat( staticCounters.get( StubFailureHandler.HANDLE_GENERIC_CONTEXT ) ).isEqualTo( 0 ); + assertThat( staticCounters.get( StubFailureHandler.HANDLE_ENTITY_INDEXING_CONTEXT ) ).isEqualTo( 0 ); + } + + @Override + protected void expectEntityIndexingFailureHandling(String entityName, String entityReferenceAsString, + String exceptionMessage, String failingOperationAsString) { + // We'll check in the assert*() method, see below. + } + + @Override + protected void assertEntityIndexingFailureHandling(String entityName, String entityReferenceAsString, + String exceptionMessage, String failingOperationAsString) { + assertThat( staticCounters.get( StubFailureHandler.CREATE ) ).isEqualTo( 1 ); + assertThat( staticCounters.get( StubFailureHandler.HANDLE_INDEX_CONTEXT ) ).isEqualTo( 0 ); + assertThat( staticCounters.get( StubFailureHandler.HANDLE_GENERIC_CONTEXT ) ).isEqualTo( 0 ); + assertThat( staticCounters.get( StubFailureHandler.HANDLE_ENTITY_INDEXING_CONTEXT ) ).isEqualTo( 1 ); + } + + @Override + protected void expectEntityGetterFailureHandling(String entityName, String entityReferenceAsString, + String exceptionMessage, String failingOperationAsString) { + // We'll check in the assert*() method, see below. + } + + @Override + protected void assertEntityGetterFailureHandling(String entityName, String entityReferenceAsString, + String exceptionMessage, String failingOperationAsString) { + assertThat( staticCounters.get( StubFailureHandler.CREATE ) ).isEqualTo( 1 ); + assertThat( staticCounters.get( StubFailureHandler.HANDLE_INDEX_CONTEXT ) ).isEqualTo( 0 ); + assertThat( staticCounters.get( StubFailureHandler.HANDLE_GENERIC_CONTEXT ) ).isEqualTo( 0 ); + assertThat( staticCounters.get( StubFailureHandler.HANDLE_ENTITY_INDEXING_CONTEXT ) ).isEqualTo( 1 ); + } + + @Override + protected void expectMassIndexerOperationFailureHandling(String exceptionMessage, String failingOperationAsString) { + // We'll check in the assert*() method, see below. + } + + @Override + protected void assertMassIndexerOperationFailureHandling(String exceptionMessage, String failingOperationAsString) { + assertThat( staticCounters.get( StubFailureHandler.CREATE ) ).isEqualTo( 1 ); + assertThat( staticCounters.get( StubFailureHandler.HANDLE_INDEX_CONTEXT ) ).isEqualTo( 0 ); + assertThat( staticCounters.get( StubFailureHandler.HANDLE_GENERIC_CONTEXT ) ).isEqualTo( 1 ); + } + + @Override + protected void expectEntityIndexingAndMassIndexerOperationFailureHandling(String entityName, + String entityReferenceAsString, + String failingEntityIndexingExceptionMessage, String failingEntityIndexingOperationAsString, + String failingMassIndexerOperationExceptionMessage, String failingMassIndexerOperationAsString) { + // We'll check in the assert*() method, see below. + } + + @Override + protected void assertEntityIndexingAndMassIndexerOperationFailureHandling(String entityName, + String entityReferenceAsString, + String failingEntityIndexingExceptionMessage, String failingEntityIndexingOperationAsString, + String failingMassIndexerOperationExceptionMessage, String failingMassIndexerOperationAsString) { + assertThat( staticCounters.get( StubFailureHandler.CREATE ) ).isEqualTo( 1 ); + assertThat( staticCounters.get( StubFailureHandler.HANDLE_INDEX_CONTEXT ) ).isEqualTo( 0 ); + assertThat( staticCounters.get( StubFailureHandler.HANDLE_GENERIC_CONTEXT ) ).isEqualTo( 1 ); + assertThat( staticCounters.get( StubFailureHandler.HANDLE_ENTITY_INDEXING_CONTEXT ) ).isEqualTo( 1 ); + } +} diff --git a/integrationtest/mapper/orm/src/test/java/org/hibernate/search/integrationtest/mapper/orm/massindexing/MassIndexingFailureCustomMassIndexingFailureHandlerIT.java b/integrationtest/mapper/orm/src/test/java/org/hibernate/search/integrationtest/mapper/orm/massindexing/MassIndexingFailureCustomMassIndexingFailureHandlerIT.java new file mode 100644 index 00000000000..d2409c1b2fd --- /dev/null +++ b/integrationtest/mapper/orm/src/test/java/org/hibernate/search/integrationtest/mapper/orm/massindexing/MassIndexingFailureCustomMassIndexingFailureHandlerIT.java @@ -0,0 +1,168 @@ +/* + * 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 . + */ +package org.hibernate.search.integrationtest.mapper.orm.massindexing; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.easymock.EasyMock.capture; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.reset; +import static org.easymock.EasyMock.verify; + +import org.hibernate.search.mapper.orm.massindexing.MassIndexingEntityFailureContext; +import org.hibernate.search.mapper.orm.massindexing.MassIndexingFailureContext; +import org.hibernate.search.mapper.orm.massindexing.MassIndexingFailureHandler; +import org.hibernate.search.util.common.SearchException; +import org.hibernate.search.util.impl.test.ExceptionMatcherBuilder; + +import org.junit.Assert; + +import org.easymock.Capture; +import org.easymock.EasyMock; + +public class MassIndexingFailureCustomMassIndexingFailureHandlerIT extends AbstractMassIndexingFailureIT { + + private final MassIndexingFailureHandler failureHandler = EasyMock.createMock( MassIndexingFailureHandler.class ); + private final Capture genericFailureContextCapture = EasyMock.newCapture(); + private final Capture entityFailureContextCapture = EasyMock.newCapture(); + + @Override + protected String getBackgroundFailureHandlerReference() { + return null; + } + + @Override + protected MassIndexingFailureHandler getMassIndexingFailureHandler() { + return failureHandler; + } + + @Override + protected void expectEntityIndexingFailureHandling(String entityName, String entityReferenceAsString, + String exceptionMessage, String failingOperationAsString) { + reset( failureHandler ); + failureHandler.handle( capture( entityFailureContextCapture ) ); + replay( failureHandler ); + } + + @Override + protected void assertEntityIndexingFailureHandling(String entityName, String entityReferenceAsString, + String exceptionMessage, String failingOperationAsString) { + verify( failureHandler ); + + MassIndexingEntityFailureContext context = entityFailureContextCapture.getValue(); + Assert.assertThat( + context.getThrowable(), + ExceptionMatcherBuilder.isException( SimulatedFailure.class ) + .withMessage( exceptionMessage ) + .build() + ); + assertThat( context.getFailingOperation() ).asString() + .isEqualTo( failingOperationAsString ); + assertThat( context.getEntityReferences() ) + .hasSize( 1 ) + .element( 0 ) + .asString() + .isEqualTo( entityReferenceAsString ); + } + + @Override + protected void expectEntityGetterFailureHandling(String entityName, String entityReferenceAsString, + String exceptionMessage, String failingOperationAsString) { + reset( failureHandler ); + failureHandler.handle( capture( entityFailureContextCapture ) ); + replay( failureHandler ); + } + + @Override + protected void assertEntityGetterFailureHandling(String entityName, String entityReferenceAsString, + String exceptionMessage, String failingOperationAsString) { + verify( failureHandler ); + + MassIndexingEntityFailureContext context = entityFailureContextCapture.getValue(); + Assert.assertThat( + context.getThrowable(), + ExceptionMatcherBuilder.isException( SearchException.class ) + .withMessage( "Exception while invoking" ) + .causedBy( SimulatedFailure.class ) + .withMessage( exceptionMessage ) + .build() + ); + assertThat( context.getFailingOperation() ).asString() + .isEqualTo( failingOperationAsString ); + assertThat( context.getEntityReferences() ) + .hasSize( 1 ) + .element( 0 ) + .asString() + .isEqualTo( entityReferenceAsString ); + } + + @Override + protected void expectMassIndexerOperationFailureHandling(String exceptionMessage, String failingOperationAsString) { + reset( failureHandler ); + failureHandler.handle( capture( genericFailureContextCapture ) ); + replay( failureHandler ); + } + + @Override + protected void assertMassIndexerOperationFailureHandling(String exceptionMessage, String failingOperationAsString) { + verify( failureHandler ); + + MassIndexingFailureContext context = genericFailureContextCapture.getValue(); + Assert.assertThat( + context.getThrowable(), + ExceptionMatcherBuilder.isException( SimulatedFailure.class ) + .withMessage( exceptionMessage ) + .build() + ); + assertThat( context.getFailingOperation() ).asString() + .isEqualTo( failingOperationAsString ); + } + + @Override + protected void expectEntityIndexingAndMassIndexerOperationFailureHandling(String entityName, + String entityReferenceAsString, + String failingEntityIndexingExceptionMessage, String failingEntityIndexingOperationAsString, + String failingMassIndexerOperationExceptionMessage, String failingMassIndexerOperationAsString) { + reset( failureHandler ); + failureHandler.handle( capture( entityFailureContextCapture ) ); + failureHandler.handle( capture( genericFailureContextCapture ) ); + replay( failureHandler ); + } + + @Override + protected void assertEntityIndexingAndMassIndexerOperationFailureHandling(String entityName, + String entityReferenceAsString, + String failingEntityIndexingExceptionMessage, String failingEntityIndexingOperationAsString, + String failingMassIndexerOperationExceptionMessage, String failingMassIndexerOperationAsString) { + verify( failureHandler ); + + MassIndexingEntityFailureContext entityFailureContext = entityFailureContextCapture.getValue(); + Assert.assertThat( + entityFailureContext.getThrowable(), + ExceptionMatcherBuilder.isException( SimulatedFailure.class ) + .withMessage( failingEntityIndexingExceptionMessage ) + .build() + ); + assertThat( entityFailureContext.getFailingOperation() ).asString() + .isEqualTo( failingEntityIndexingOperationAsString ); + assertThat( entityFailureContext.getEntityReferences() ) + .hasSize( 1 ) + .element( 0 ) + .asString() + .isEqualTo( entityReferenceAsString ); + + + MassIndexingFailureContext massIndexerOperationFailureContext = genericFailureContextCapture.getValue(); + Assert.assertThat( + massIndexerOperationFailureContext.getThrowable(), + ExceptionMatcherBuilder.isException( SimulatedFailure.class ) + .withMessage( failingMassIndexerOperationExceptionMessage ) + .build() + ); + assertThat( massIndexerOperationFailureContext.getFailingOperation() ).asString() + .isEqualTo( failingMassIndexerOperationAsString ); + } +} diff --git a/integrationtest/mapper/orm/src/test/java/org/hibernate/search/integrationtest/mapper/orm/massindexing/MassIndexingFailureDefaultBackgroundFailureHandlerIT.java b/integrationtest/mapper/orm/src/test/java/org/hibernate/search/integrationtest/mapper/orm/massindexing/MassIndexingFailureDefaultBackgroundFailureHandlerIT.java new file mode 100644 index 00000000000..e9109ea02e3 --- /dev/null +++ b/integrationtest/mapper/orm/src/test/java/org/hibernate/search/integrationtest/mapper/orm/massindexing/MassIndexingFailureDefaultBackgroundFailureHandlerIT.java @@ -0,0 +1,127 @@ +/* + * 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 . + */ +package org.hibernate.search.integrationtest.mapper.orm.massindexing; + +import org.hibernate.search.mapper.orm.massindexing.MassIndexingFailureHandler; +import org.hibernate.search.util.common.SearchException; +import org.hibernate.search.util.impl.test.ExceptionMatcherBuilder; +import org.hibernate.search.util.impl.test.rule.ExpectedLog4jLog; + +import org.junit.Rule; + +import org.apache.log4j.Level; + +public class MassIndexingFailureDefaultBackgroundFailureHandlerIT extends AbstractMassIndexingFailureIT { + + @Rule + public ExpectedLog4jLog logged = ExpectedLog4jLog.create(); + + @Override + protected String getBackgroundFailureHandlerReference() { + return null; + } + + @Override + protected MassIndexingFailureHandler getMassIndexingFailureHandler() { + return null; + } + + @Override + protected void expectEntityIndexingFailureHandling(String entityName, String entityReferenceAsString, + String exceptionMessage, String failingOperationAsString) { + logged.expectEvent( + Level.ERROR, + ExceptionMatcherBuilder.isException( SimulatedFailure.class ) + .withMessage( exceptionMessage ) + .build(), + failingOperationAsString, + "Entities that could not be indexed correctly:", + entityReferenceAsString + ) + .once(); + } + + @Override + protected void assertEntityIndexingFailureHandling(String entityName, String entityReferenceAsString, + String exceptionMessage, String failingOperationAsString) { + // If we get there, everything works fine. + } + + @Override + protected void expectEntityGetterFailureHandling(String entityName, String entityReferenceAsString, + String exceptionMessage, String failingOperationAsString) { + logged.expectEvent( + Level.ERROR, + ExceptionMatcherBuilder.isException( SearchException.class ) + .withMessage( "Exception while invoking" ) + .causedBy( SimulatedFailure.class ) + .withMessage( exceptionMessage ) + .build(), + failingOperationAsString, + "Entities that could not be indexed correctly:", + entityReferenceAsString + ) + .once(); + } + + @Override + protected void assertEntityGetterFailureHandling(String entityName, String entityReferenceAsString, + String exceptionMessage, String failingOperationAsString) { + // If we get there, everything works fine. + } + + @Override + protected void expectMassIndexerOperationFailureHandling(String exceptionMessage, String failingOperationAsString) { + logged.expectEvent( + Level.ERROR, + ExceptionMatcherBuilder.isException( SimulatedFailure.class ) + .withMessage( exceptionMessage ) + .build(), + failingOperationAsString + ) + .once(); + } + + @Override + protected void assertMassIndexerOperationFailureHandling(String exceptionMessage, String failingOperationAsString) { + // If we get there, everything works fine. + } + + @Override + protected void expectEntityIndexingAndMassIndexerOperationFailureHandling(String entityName, + String entityReferenceAsString, + String failingEntityIndexingExceptionMessage, String failingEntityIndexingOperationAsString, + String failingMassIndexerOperationExceptionMessage, String failingMassIndexerOperationAsString) { + logged.expectEvent( + Level.ERROR, + ExceptionMatcherBuilder.isException( SimulatedFailure.class ) + .withMessage( failingEntityIndexingExceptionMessage ) + .build(), + failingEntityIndexingOperationAsString, + "Entities that could not be indexed correctly:", + entityReferenceAsString + ) + .once(); + + logged.expectEvent( + Level.ERROR, + ExceptionMatcherBuilder.isException( SimulatedFailure.class ) + .withMessage( failingMassIndexerOperationExceptionMessage ) + .build(), + failingMassIndexerOperationAsString + ) + .once(); + } + + @Override + protected void assertEntityIndexingAndMassIndexerOperationFailureHandling(String entityName, + String entityReferenceAsString, + String failingEntityIndexingExceptionMessage, String failingEntityIndexingOperationAsString, + String failingMassIndexerOperationExceptionMessage, String failingMassIndexerOperationAsString) { + // If we get there, everything works fine. + } +} diff --git a/integrationtest/mapper/orm/src/test/java/org/hibernate/search/integrationtest/mapper/orm/massindexing/MassIndexingFailureIT.java b/integrationtest/mapper/orm/src/test/java/org/hibernate/search/integrationtest/mapper/orm/massindexing/MassIndexingFailureIT.java deleted file mode 100644 index 6f8c8c1c5b0..00000000000 --- a/integrationtest/mapper/orm/src/test/java/org/hibernate/search/integrationtest/mapper/orm/massindexing/MassIndexingFailureIT.java +++ /dev/null @@ -1,842 +0,0 @@ -/* - * 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 . - */ -package org.hibernate.search.integrationtest.mapper.orm.massindexing; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Fail.fail; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; -import java.util.function.Function; -import javax.persistence.Entity; -import javax.persistence.Id; - -import org.hibernate.SessionFactory; -import org.hibernate.search.engine.backend.work.execution.DocumentCommitStrategy; -import org.hibernate.search.engine.backend.work.execution.DocumentRefreshStrategy; -import org.hibernate.search.engine.cfg.EngineSettings; -import org.hibernate.search.engine.cfg.spi.EngineSpiSettings; -import org.hibernate.search.mapper.orm.Search; -import org.hibernate.search.mapper.orm.automaticindexing.AutomaticIndexingStrategyName; -import org.hibernate.search.mapper.orm.cfg.HibernateOrmMapperSettings; -import org.hibernate.search.mapper.orm.massindexing.MassIndexer; -import org.hibernate.search.mapper.orm.session.SearchSession; -import org.hibernate.search.mapper.pojo.mapping.definition.annotation.GenericField; -import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed; -import org.hibernate.search.util.common.SearchException; -import org.hibernate.search.util.impl.integrationtest.common.rule.BackendMock; -import org.hibernate.search.util.impl.integrationtest.common.rule.ThreadSpy; -import org.hibernate.search.util.impl.integrationtest.common.stub.StubFailureHandler; -import org.hibernate.search.util.impl.integrationtest.common.stub.backend.index.StubIndexScopeWork; -import org.hibernate.search.util.impl.integrationtest.mapper.orm.OrmSetupHelper; -import org.hibernate.search.util.impl.integrationtest.mapper.orm.OrmUtils; -import org.hibernate.search.util.impl.test.ExceptionMatcherBuilder; -import org.hibernate.search.util.impl.test.SubTest; -import org.hibernate.search.util.impl.test.rule.ExpectedLog4jLog; -import org.hibernate.search.util.impl.test.rule.StaticCounters; - -import org.junit.Rule; -import org.junit.Test; - -import org.apache.log4j.Level; -import org.assertj.core.api.InstanceOfAssertFactories; -import org.awaitility.Awaitility; - -public class MassIndexingFailureIT { - - public static final String TITLE_1 = "Oliver Twist"; - public static final String AUTHOR_1 = "Charles Dickens"; - public static final String TITLE_2 = "Ulysses"; - public static final String AUTHOR_2 = "James Joyce"; - public static final String TITLE_3 = "Frankenstein"; - public static final String AUTHOR_3 = "Mary Shelley"; - - @Rule - public BackendMock backendMock = new BackendMock( "stubBackend" ); - - @Rule - public OrmSetupHelper ormSetupHelper = OrmSetupHelper.withBackendMock( backendMock ); - - @Rule - public ExpectedLog4jLog logged = ExpectedLog4jLog.create(); - - @Rule - public StaticCounters staticCounters = new StaticCounters(); - - @Rule - public ThreadSpy threadSpy = new ThreadSpy(); - - @Test - public void indexing_defaultHandler() { - SessionFactory sessionFactory = setup( null ); - - logged.expectEvent( - Level.ERROR, - ExceptionMatcherBuilder.isException( SimulatedFailure.class ) - .withMessage( "Indexing failure" ) - .build(), - "Indexing instance of entity '" + Book.NAME + "'", - "Entities that could not be indexed correctly:", - Book.NAME + "#2" - ) - .once(); - - doMassIndexingWithFailure( - sessionFactory, - ThreadExpectation.CREATED_AND_TERMINATED, - throwable -> assertThat( throwable ).isInstanceOf( SearchException.class ) - .hasMessageContainingAll( - "1 entities could not be indexed", - "See the logs for details.", - "First failure on entity 'Book#2': ", - "Indexing failure" - ) - .hasCauseInstanceOf( SimulatedFailure.class ), - expectIndexScopeWork( StubIndexScopeWork.Type.PURGE, ExecutionExpectation.SUCCEED ), - expectIndexScopeWork( StubIndexScopeWork.Type.MERGE_SEGMENTS, ExecutionExpectation.SUCCEED ), - expectIndexingWorks( ExecutionExpectation.FAIL ), - expectIndexScopeWork( StubIndexScopeWork.Type.FLUSH, ExecutionExpectation.SUCCEED ) - ); - } - - @Test - public void indexing_customHandler() { - assertThat( staticCounters.get( StubFailureHandler.CREATE ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_INDEX_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_GENERIC_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_ENTITY_INDEXING_CONTEXT ) ).isEqualTo( 0 ); - - SessionFactory sessionFactory = setup( StubFailureHandler.class.getName() ); - - assertThat( staticCounters.get( StubFailureHandler.CREATE ) ).isEqualTo( 1 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_INDEX_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_GENERIC_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_ENTITY_INDEXING_CONTEXT ) ).isEqualTo( 0 ); - - doMassIndexingWithFailure( - sessionFactory, - ThreadExpectation.CREATED_AND_TERMINATED, - throwable -> assertThat( throwable ).isInstanceOf( SearchException.class ) - .hasMessageContainingAll( - "1 entities could not be indexed", - "See the logs for details.", - "First failure on entity 'Book#2': ", - "Indexing failure" - ), - expectIndexScopeWork( StubIndexScopeWork.Type.PURGE, ExecutionExpectation.SUCCEED ), - expectIndexScopeWork( StubIndexScopeWork.Type.MERGE_SEGMENTS, ExecutionExpectation.SUCCEED ), - expectIndexingWorks( ExecutionExpectation.FAIL ), - expectIndexScopeWork( StubIndexScopeWork.Type.FLUSH, ExecutionExpectation.SUCCEED ) - ); - - assertThat( staticCounters.get( StubFailureHandler.CREATE ) ).isEqualTo( 1 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_INDEX_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_GENERIC_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_ENTITY_INDEXING_CONTEXT ) ).isEqualTo( 1 ); - } - - @Test - public void getId_defaultHandler() { - SessionFactory sessionFactory = setup( null ); - - logged.expectEvent( - Level.ERROR, - ExceptionMatcherBuilder.isException( SearchException.class ) - .withMessage( "Exception while invoking" ) - .causedBy( SimulatedFailure.class ) - .withMessage( "getId failure" ) - .build(), - "Indexing instance of entity '" + Book.NAME + "'", - "Entities that could not be indexed correctly:", - Book.NAME + "#2" - - ) - .once(); - - doMassIndexingWithBook2GetIdFailure( sessionFactory ); - } - - @Test - public void getId_customHandler() { - assertThat( staticCounters.get( StubFailureHandler.CREATE ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_INDEX_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_GENERIC_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_ENTITY_INDEXING_CONTEXT ) ).isEqualTo( 0 ); - - SessionFactory sessionFactory = setup( StubFailureHandler.class.getName() ); - - assertThat( staticCounters.get( StubFailureHandler.CREATE ) ).isEqualTo( 1 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_INDEX_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_GENERIC_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_ENTITY_INDEXING_CONTEXT ) ).isEqualTo( 0 ); - - doMassIndexingWithBook2GetIdFailure( sessionFactory ); - - assertThat( staticCounters.get( StubFailureHandler.CREATE ) ).isEqualTo( 1 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_INDEX_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_GENERIC_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_ENTITY_INDEXING_CONTEXT ) ).isEqualTo( 1 ); - } - - @Test - public void getTitle_defaultHandler() { - SessionFactory sessionFactory = setup( null ); - - logged.expectEvent( - Level.ERROR, - ExceptionMatcherBuilder.isException( SearchException.class ) - .withMessage( "Exception while invoking" ) - .causedBy( SimulatedFailure.class ) - .withMessage( "getTitle failure" ) - .build(), - "Indexing instance of entity '" + Book.NAME + "'", - "Entities that could not be indexed correctly:", - Book.NAME + "#2" - ) - .once(); - - doMassIndexingWithBook2GetTitleFailure( sessionFactory ); - } - - @Test - public void getTitle_customHandler() { - assertThat( staticCounters.get( StubFailureHandler.CREATE ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_INDEX_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_GENERIC_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_ENTITY_INDEXING_CONTEXT ) ).isEqualTo( 0 ); - - SessionFactory sessionFactory = setup( StubFailureHandler.class.getName() ); - - assertThat( staticCounters.get( StubFailureHandler.CREATE ) ).isEqualTo( 1 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_INDEX_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_GENERIC_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_ENTITY_INDEXING_CONTEXT ) ).isEqualTo( 0 ); - - doMassIndexingWithBook2GetTitleFailure( sessionFactory ); - - assertThat( staticCounters.get( StubFailureHandler.CREATE ) ).isEqualTo( 1 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_INDEX_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_GENERIC_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_ENTITY_INDEXING_CONTEXT ) ).isEqualTo( 1 ); - } - - @Test - public void purge_defaultHandler() { - SessionFactory sessionFactory = setup( null ); - - logged.expectEvent( - Level.ERROR, - ExceptionMatcherBuilder.isException( SimulatedFailure.class ) - .withMessage( "PURGE failure" ) - .build(), - "MassIndexer operation" - ) - .once(); - - doMassIndexingWithFailure( - sessionFactory, - ThreadExpectation.NOT_CREATED, - throwable -> assertThat( throwable ).isInstanceOf( SimulatedFailure.class ) - .hasMessageContaining( "PURGE failure" ), - expectIndexScopeWork( StubIndexScopeWork.Type.PURGE, ExecutionExpectation.FAIL ) - ); - } - - @Test - public void purge_customHandler() { - assertThat( staticCounters.get( StubFailureHandler.CREATE ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_INDEX_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_GENERIC_CONTEXT ) ).isEqualTo( 0 ); - - SessionFactory sessionFactory = setup( StubFailureHandler.class.getName() ); - - assertThat( staticCounters.get( StubFailureHandler.CREATE ) ).isEqualTo( 1 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_INDEX_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_GENERIC_CONTEXT ) ).isEqualTo( 0 ); - - doMassIndexingWithFailure( - sessionFactory, - ThreadExpectation.NOT_CREATED, - throwable -> assertThat( throwable ).isInstanceOf( SimulatedFailure.class ) - .hasMessageContaining( "PURGE failure" ), - expectIndexScopeWork( StubIndexScopeWork.Type.PURGE, ExecutionExpectation.FAIL ) - ); - - assertThat( staticCounters.get( StubFailureHandler.CREATE ) ).isEqualTo( 1 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_INDEX_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_GENERIC_CONTEXT ) ).isEqualTo( 1 ); - } - - @Test - public void mergeSegmentsBefore_defaultHandler() { - SessionFactory sessionFactory = setup( null ); - - logged.expectEvent( - Level.ERROR, - ExceptionMatcherBuilder.isException( SimulatedFailure.class ) - .withMessage( "MERGE_SEGMENTS failure" ) - .build(), - "MassIndexer operation" - ) - .once(); - - doMassIndexingWithFailure( - sessionFactory, - ThreadExpectation.NOT_CREATED, - throwable -> assertThat( throwable ).isInstanceOf( SimulatedFailure.class ) - .hasMessageContaining( "MERGE_SEGMENTS failure" ), - expectIndexScopeWork( StubIndexScopeWork.Type.PURGE, ExecutionExpectation.SUCCEED ), - expectIndexScopeWork( StubIndexScopeWork.Type.MERGE_SEGMENTS, ExecutionExpectation.FAIL ) - ); - } - - @Test - public void mergeSegmentsBefore_customHandler() { - assertThat( staticCounters.get( StubFailureHandler.CREATE ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_INDEX_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_GENERIC_CONTEXT ) ).isEqualTo( 0 ); - - SessionFactory sessionFactory = setup( StubFailureHandler.class.getName() ); - - assertThat( staticCounters.get( StubFailureHandler.CREATE ) ).isEqualTo( 1 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_INDEX_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_GENERIC_CONTEXT ) ).isEqualTo( 0 ); - - doMassIndexingWithFailure( - sessionFactory, - ThreadExpectation.NOT_CREATED, - throwable -> assertThat( throwable ).isInstanceOf( SimulatedFailure.class ) - .hasMessageContaining( "MERGE_SEGMENTS failure" ), - expectIndexScopeWork( StubIndexScopeWork.Type.PURGE, ExecutionExpectation.SUCCEED ), - expectIndexScopeWork( StubIndexScopeWork.Type.MERGE_SEGMENTS, ExecutionExpectation.FAIL ) - ); - - assertThat( staticCounters.get( StubFailureHandler.CREATE ) ).isEqualTo( 1 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_INDEX_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_GENERIC_CONTEXT ) ).isEqualTo( 1 ); - } - - @Test - public void mergeSegmentsAfter_defaultHandler() { - SessionFactory sessionFactory = setup( null ); - - logged.expectEvent( - Level.ERROR, - ExceptionMatcherBuilder.isException( SimulatedFailure.class ) - .withMessage( "MERGE_SEGMENTS failure" ) - .build(), - "MassIndexer operation" - ) - .once(); - - doMassIndexingWithFailure( - sessionFactory, - searchSession -> searchSession.massIndexer().mergeSegmentsOnFinish( true ), - ThreadExpectation.CREATED_AND_TERMINATED, - throwable -> assertThat( throwable ).isInstanceOf( SimulatedFailure.class ) - .hasMessageContaining( "MERGE_SEGMENTS failure" ), - expectIndexScopeWork( StubIndexScopeWork.Type.PURGE, ExecutionExpectation.SUCCEED ), - expectIndexScopeWork( StubIndexScopeWork.Type.MERGE_SEGMENTS, ExecutionExpectation.SUCCEED ), - expectIndexingWorks( ExecutionExpectation.SUCCEED ), - expectIndexScopeWork( StubIndexScopeWork.Type.MERGE_SEGMENTS, ExecutionExpectation.FAIL ) - ); - } - - @Test - public void mergeSegmentsAfter_customHandler() { - assertThat( staticCounters.get( StubFailureHandler.CREATE ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_INDEX_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_GENERIC_CONTEXT ) ).isEqualTo( 0 ); - - SessionFactory sessionFactory = setup( StubFailureHandler.class.getName() ); - - assertThat( staticCounters.get( StubFailureHandler.CREATE ) ).isEqualTo( 1 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_INDEX_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_GENERIC_CONTEXT ) ).isEqualTo( 0 ); - - doMassIndexingWithFailure( - sessionFactory, - searchSession -> searchSession.massIndexer().mergeSegmentsOnFinish( true ), - ThreadExpectation.CREATED_AND_TERMINATED, - throwable -> assertThat( throwable ).isInstanceOf( SimulatedFailure.class ) - .hasMessageContaining( "MERGE_SEGMENTS failure" ), - expectIndexScopeWork( StubIndexScopeWork.Type.PURGE, ExecutionExpectation.SUCCEED ), - expectIndexScopeWork( StubIndexScopeWork.Type.MERGE_SEGMENTS, ExecutionExpectation.SUCCEED ), - expectIndexingWorks( ExecutionExpectation.SUCCEED ), - expectIndexScopeWork( StubIndexScopeWork.Type.MERGE_SEGMENTS, ExecutionExpectation.FAIL ) - ); - - assertThat( staticCounters.get( StubFailureHandler.CREATE ) ).isEqualTo( 1 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_INDEX_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_GENERIC_CONTEXT ) ).isEqualTo( 1 ); - } - - @Test - public void flush_defaultHandler() { - SessionFactory sessionFactory = setup( null ); - - logged.expectEvent( - Level.ERROR, - ExceptionMatcherBuilder.isException( SimulatedFailure.class ) - .withMessage( "FLUSH failure" ) - .build(), - "MassIndexer operation" - ) - .once(); - - doMassIndexingWithFailure( - sessionFactory, - ThreadExpectation.CREATED_AND_TERMINATED, - throwable -> assertThat( throwable ).isInstanceOf( SimulatedFailure.class ) - .hasMessageContaining( "FLUSH failure" ), - expectIndexScopeWork( StubIndexScopeWork.Type.PURGE, ExecutionExpectation.SUCCEED ), - expectIndexScopeWork( StubIndexScopeWork.Type.MERGE_SEGMENTS, ExecutionExpectation.SUCCEED ), - expectIndexingWorks( ExecutionExpectation.SUCCEED ), - expectIndexScopeWork( StubIndexScopeWork.Type.FLUSH, ExecutionExpectation.FAIL ) - ); - } - - @Test - public void flush_customHandler() { - assertThat( staticCounters.get( StubFailureHandler.CREATE ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_INDEX_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_GENERIC_CONTEXT ) ).isEqualTo( 0 ); - - SessionFactory sessionFactory = setup( StubFailureHandler.class.getName() ); - - assertThat( staticCounters.get( StubFailureHandler.CREATE ) ).isEqualTo( 1 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_INDEX_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_GENERIC_CONTEXT ) ).isEqualTo( 0 ); - - doMassIndexingWithFailure( - sessionFactory, - ThreadExpectation.CREATED_AND_TERMINATED, - throwable -> assertThat( throwable ).isInstanceOf( SimulatedFailure.class ) - .hasMessageContaining( "FLUSH failure" ), - expectIndexScopeWork( StubIndexScopeWork.Type.PURGE, ExecutionExpectation.SUCCEED ), - expectIndexScopeWork( StubIndexScopeWork.Type.MERGE_SEGMENTS, ExecutionExpectation.SUCCEED ), - expectIndexingWorks( ExecutionExpectation.SUCCEED ), - expectIndexScopeWork( StubIndexScopeWork.Type.FLUSH, ExecutionExpectation.FAIL ) - ); - - assertThat( staticCounters.get( StubFailureHandler.CREATE ) ).isEqualTo( 1 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_INDEX_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_GENERIC_CONTEXT ) ).isEqualTo( 1 ); - } - - @Test - public void indexingAndFlush_defaultHandler() { - SessionFactory sessionFactory = setup( null ); - - logged.expectEvent( - Level.ERROR, - ExceptionMatcherBuilder.isException( SimulatedFailure.class ) - .withMessage( "Indexing failure" ) - .build(), - "Indexing instance of entity '" + Book.NAME + "'", - "Entities that could not be indexed correctly:", - Book.NAME + "#2" - ) - .once(); - - logged.expectEvent( - Level.ERROR, - ExceptionMatcherBuilder.isException( SimulatedFailure.class ) - .withMessage( "FLUSH failure" ) - .build(), - "MassIndexer operation" - ) - .once(); - - doMassIndexingWithFailure( - sessionFactory, - ThreadExpectation.CREATED_AND_TERMINATED, - throwable -> assertThat( throwable ).isInstanceOf( SimulatedFailure.class ) - .hasMessageContaining( "FLUSH failure" ) - // Indexing failure should also be mentioned as a suppressed exception - .extracting( Throwable::getSuppressed ).asInstanceOf( InstanceOfAssertFactories.ARRAY ) - .anySatisfy( suppressed -> assertThat( suppressed ).asInstanceOf( InstanceOfAssertFactories.THROWABLE ) - .isInstanceOf( SearchException.class ) - .hasMessageContainingAll( - "1 entities could not be indexed", - "See the logs for details.", - "First failure on entity 'Book#2': ", - "Indexing failure" - ) - .hasCauseInstanceOf( SimulatedFailure.class ) - ), - expectIndexScopeWork( StubIndexScopeWork.Type.PURGE, ExecutionExpectation.SUCCEED ), - expectIndexScopeWork( StubIndexScopeWork.Type.MERGE_SEGMENTS, ExecutionExpectation.SUCCEED ), - expectIndexingWorks( ExecutionExpectation.FAIL ), - expectIndexScopeWork( StubIndexScopeWork.Type.FLUSH, ExecutionExpectation.FAIL ) - ); - } - - @Test - public void indexingAndFlush_customHandler() { - assertThat( staticCounters.get( StubFailureHandler.CREATE ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_INDEX_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_GENERIC_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_ENTITY_INDEXING_CONTEXT ) ).isEqualTo( 0 ); - - SessionFactory sessionFactory = setup( StubFailureHandler.class.getName() ); - - assertThat( staticCounters.get( StubFailureHandler.CREATE ) ).isEqualTo( 1 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_INDEX_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_GENERIC_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_ENTITY_INDEXING_CONTEXT ) ).isEqualTo( 0 ); - - doMassIndexingWithFailure( - sessionFactory, - ThreadExpectation.CREATED_AND_TERMINATED, - throwable -> assertThat( throwable ).isInstanceOf( SimulatedFailure.class ) - .hasMessageContaining( "FLUSH failure" ) - // Indexing failure should also be mentioned as a suppressed exception - .extracting( Throwable::getSuppressed ).asInstanceOf( InstanceOfAssertFactories.ARRAY ) - .anySatisfy( suppressed -> assertThat( suppressed ).asInstanceOf( InstanceOfAssertFactories.THROWABLE ) - .isInstanceOf( SearchException.class ) - .hasMessageContainingAll( - "1 entities could not be indexed", - "See the logs for details.", - "First failure on entity 'Book#2': ", - "Indexing failure" - ) - .hasCauseInstanceOf( SimulatedFailure.class ) - ), - expectIndexScopeWork( StubIndexScopeWork.Type.PURGE, ExecutionExpectation.SUCCEED ), - expectIndexScopeWork( StubIndexScopeWork.Type.MERGE_SEGMENTS, ExecutionExpectation.SUCCEED ), - expectIndexingWorks( ExecutionExpectation.FAIL ), - expectIndexScopeWork( StubIndexScopeWork.Type.FLUSH, ExecutionExpectation.FAIL ) - ); - - assertThat( staticCounters.get( StubFailureHandler.CREATE ) ).isEqualTo( 1 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_INDEX_CONTEXT ) ).isEqualTo( 0 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_GENERIC_CONTEXT ) ).isEqualTo( 1 ); - assertThat( staticCounters.get( StubFailureHandler.HANDLE_ENTITY_INDEXING_CONTEXT ) ).isEqualTo( 1 ); - } - - private void doMassIndexingWithFailure(SessionFactory sessionFactory, - ThreadExpectation threadExpectation, - Consumer thrownExpectation, - Runnable ... expectationSetters) { - doMassIndexingWithFailure( - sessionFactory, - searchSession -> searchSession.massIndexer(), - threadExpectation, - thrownExpectation, - expectationSetters - ); - } - - private void doMassIndexingWithFailure(SessionFactory sessionFactory, - Function indexerProducer, - ThreadExpectation threadExpectation, - Consumer thrownExpectation, - Runnable ... expectationSetters) { - doMassIndexingWithFailure( - sessionFactory, - indexerProducer, - threadExpectation, - thrownExpectation, - ExecutionExpectation.SUCCEED, ExecutionExpectation.SUCCEED, - expectationSetters - ); - } - - private void doMassIndexingWithBook2GetIdFailure(SessionFactory sessionFactory) { - doMassIndexingWithFailure( - sessionFactory, - searchSession -> searchSession.massIndexer(), - ThreadExpectation.CREATED_AND_TERMINATED, - throwable -> assertThat( throwable ).isInstanceOf( SearchException.class ) - .hasMessageContainingAll( - "1 entities could not be indexed", - "See the logs for details.", - "First failure on entity 'Book#2': ", - "Exception while invoking" - ) - .extracting( Throwable::getCause ).asInstanceOf( InstanceOfAssertFactories.THROWABLE ) - .isInstanceOf( SearchException.class ) - .hasMessageContaining( "Exception while invoking" ), - ExecutionExpectation.FAIL, ExecutionExpectation.SKIP, - expectIndexScopeWork( StubIndexScopeWork.Type.PURGE, ExecutionExpectation.SUCCEED ), - expectIndexScopeWork( StubIndexScopeWork.Type.MERGE_SEGMENTS, ExecutionExpectation.SUCCEED ), - expectIndexingWorks( ExecutionExpectation.SKIP ), - expectIndexScopeWork( StubIndexScopeWork.Type.FLUSH, ExecutionExpectation.SUCCEED ) - ); - } - - private void doMassIndexingWithBook2GetTitleFailure(SessionFactory sessionFactory) { - doMassIndexingWithFailure( - sessionFactory, - searchSession -> searchSession.massIndexer(), - ThreadExpectation.CREATED_AND_TERMINATED, - throwable -> assertThat( throwable ).isInstanceOf( SearchException.class ) - .hasMessageContainingAll( - "1 entities could not be indexed", - "See the logs for details.", - "First failure on entity 'Book#2': ", - "Exception while invoking" - ) - .extracting( Throwable::getCause ).asInstanceOf( InstanceOfAssertFactories.THROWABLE ) - .isInstanceOf( SearchException.class ) - .hasMessageContaining( "Exception while invoking" ), - ExecutionExpectation.SUCCEED, ExecutionExpectation.FAIL, - expectIndexScopeWork( StubIndexScopeWork.Type.PURGE, ExecutionExpectation.SUCCEED ), - expectIndexScopeWork( StubIndexScopeWork.Type.MERGE_SEGMENTS, ExecutionExpectation.SUCCEED ), - expectIndexingWorks( ExecutionExpectation.SKIP ), - expectIndexScopeWork( StubIndexScopeWork.Type.FLUSH, ExecutionExpectation.SUCCEED ) - ); - } - - private void doMassIndexingWithFailure(SessionFactory sessionFactory, - Function indexerProducer, - ThreadExpectation threadExpectation, - Consumer thrownExpectation, - ExecutionExpectation book2GetIdExpectation, ExecutionExpectation book2GetTitleExpectation, - Runnable ... expectationSetters) { - Book.failOnBook2GetId.set( ExecutionExpectation.FAIL.equals( book2GetIdExpectation ) ); - Book.failOnBook2GetTitle.set( ExecutionExpectation.FAIL.equals( book2GetTitleExpectation ) ); - AssertionError assertionError = null; - try { - OrmUtils.withinSession( sessionFactory, session -> { - SearchSession searchSession = Search.session( session ); - MassIndexer indexer = indexerProducer.apply( searchSession ); - - for ( Runnable expectationSetter : expectationSetters ) { - expectationSetter.run(); - } - - // TODO HSEARCH-3728 simplify this when even indexing exceptions are propagated - Runnable runnable = () -> { - try { - indexer.startAndWait(); - } - catch (InterruptedException e) { - fail( "Unexpected InterruptedException: " + e.getMessage() ); - } - }; - if ( thrownExpectation == null ) { - runnable.run(); - } - else { - SubTest.expectException( runnable ) - .assertThrown() - .satisfies( thrownExpectation ); - } - } ); - backendMock.verifyExpectationsMet(); - } - catch (AssertionError e) { - assertionError = e; - throw e; - } - finally { - Book.failOnBook2GetId.set( false ); - Book.failOnBook2GetTitle.set( false ); - - if ( assertionError == null ) { - switch ( threadExpectation ) { - case CREATED_AND_TERMINATED: - Awaitility.await().untilAsserted( - () -> assertThat( threadSpy.getCreatedThreads( "mass index" ) ) - .as( "Mass indexing threads" ) - .isNotEmpty() - .allSatisfy( t -> assertThat( t ) - .extracting( Thread::getState ) - .isEqualTo( Thread.State.TERMINATED ) - ) - ); - break; - case NOT_CREATED: - assertThat( threadSpy.getCreatedThreads( "mass index" ) ) - .as( "Mass indexing threads" ) - .isEmpty(); - break; - } - } - } - } - - private Runnable expectIndexScopeWork(StubIndexScopeWork.Type type, ExecutionExpectation executionExpectation) { - return () -> { - switch ( executionExpectation ) { - case SUCCEED: - backendMock.expectIndexScopeWorks( Book.NAME ) - .indexScopeWork( type ); - break; - case FAIL: - CompletableFuture failingFuture = new CompletableFuture<>(); - failingFuture.completeExceptionally( new SimulatedFailure( type.name() + " failure" ) ); - backendMock.expectIndexScopeWorks( Book.NAME ) - .indexScopeWork( type, failingFuture ); - break; - case SKIP: - break; - } - }; - } - - private Runnable expectIndexingWorks(ExecutionExpectation workTwoExecutionExpectation) { - return () -> { - switch ( workTwoExecutionExpectation ) { - case SUCCEED: - backendMock.expectWorksAnyOrder( - Book.NAME, DocumentCommitStrategy.NONE, DocumentRefreshStrategy.NONE - ) - .add( "1", b -> b - .field( "title", TITLE_1 ) - .field( "author", AUTHOR_1 ) - ) - .add( "2", b -> b - .field( "title", TITLE_2 ) - .field( "author", AUTHOR_2 ) - ) - .add( "3", b -> b - .field( "title", TITLE_3 ) - .field( "author", AUTHOR_3 ) - ) - .processedThenExecuted(); - break; - case FAIL: - CompletableFuture failingFuture = new CompletableFuture<>(); - failingFuture.completeExceptionally( new SimulatedFailure( "Indexing failure" ) ); - backendMock.expectWorksAnyOrder( - Book.NAME, DocumentCommitStrategy.NONE, DocumentRefreshStrategy.NONE - ) - .add( "1", b -> b - .field( "title", TITLE_1 ) - .field( "author", AUTHOR_1 ) - ) - .add( "3", b -> b - .field( "title", TITLE_3 ) - .field( "author", AUTHOR_3 ) - ) - .processedThenExecuted(); - backendMock.expectWorksAnyOrder( - Book.NAME, DocumentCommitStrategy.NONE, DocumentRefreshStrategy.NONE - ) - .add( "2", b -> b - .field( "title", TITLE_2 ) - .field( "author", AUTHOR_2 ) - ) - .processedThenExecuted( failingFuture ); - break; - case SKIP: - backendMock.expectWorksAnyOrder( - Book.NAME, DocumentCommitStrategy.NONE, DocumentRefreshStrategy.NONE - ) - .add( "1", b -> b - .field( "title", TITLE_1 ) - .field( "author", AUTHOR_1 ) - ) - .add( "3", b -> b - .field( "title", TITLE_3 ) - .field( "author", AUTHOR_3 ) - ) - .processedThenExecuted(); - break; - } - }; - } - - private SessionFactory setup(String failureHandler) { - backendMock.expectAnySchema( Book.NAME ); - - SessionFactory sessionFactory = ormSetupHelper.start() - .withPropertyRadical( HibernateOrmMapperSettings.Radicals.AUTOMATIC_INDEXING_STRATEGY, AutomaticIndexingStrategyName.NONE ) - .withPropertyRadical( EngineSettings.Radicals.BACKGROUND_FAILURE_HANDLER, failureHandler ) - .withPropertyRadical( EngineSpiSettings.Radicals.THREAD_PROVIDER, threadSpy.getThreadProvider() ) - .setup( Book.class ); - - backendMock.verifyExpectationsMet(); - - OrmUtils.withinTransaction( sessionFactory, session -> { - session.persist( new Book( 1, TITLE_1, AUTHOR_1 ) ); - session.persist( new Book( 2, TITLE_2, AUTHOR_2 ) ); - session.persist( new Book( 3, TITLE_3, AUTHOR_3 ) ); - } ); - - return sessionFactory; - } - - private enum ExecutionExpectation { - SUCCEED, - FAIL, - SKIP; - } - - private enum ThreadExpectation { - CREATED_AND_TERMINATED, - NOT_CREATED; - } - - @Entity(name = Book.NAME) - @Indexed(index = Book.NAME) - public static class Book { - - public static final String NAME = "Book"; - - private static final AtomicBoolean failOnBook2GetId = new AtomicBoolean( false ); - private static final AtomicBoolean failOnBook2GetTitle = new AtomicBoolean( false ); - - private Integer id; - - private String title; - - private String author; - - public Book() { - } - - public Book(Integer id, String title, String author) { - this.id = id; - this.title = title; - this.author = author; - } - - @Id // This must be on the getter, so that Hibernate Search uses getters instead of direct field access - public Integer getId() { - if ( id == 2 && failOnBook2GetId.get() ) { - throw new SimulatedFailure( "getId failure" ); - } - return id; - } - - public void setId(Integer id) { - this.id = id; - } - - @GenericField - public String getTitle() { - if ( id == 2 && failOnBook2GetTitle.get() ) { - throw new SimulatedFailure( "getTitle failure" ); - } - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - @GenericField - public String getAuthor() { - return author; - } - - public void setAuthor(String author) { - this.author = author; - } - } - - private static class SimulatedFailure extends RuntimeException { - SimulatedFailure(String message) { - super( message ); - } - } -} diff --git a/integrationtest/mapper/orm/src/test/java/org/hibernate/search/integrationtest/mapper/orm/massindexing/MassIndexingMonitorIT.java b/integrationtest/mapper/orm/src/test/java/org/hibernate/search/integrationtest/mapper/orm/massindexing/MassIndexingMonitorIT.java index 91f6d176742..31beb04a5d0 100644 --- a/integrationtest/mapper/orm/src/test/java/org/hibernate/search/integrationtest/mapper/orm/massindexing/MassIndexingMonitorIT.java +++ b/integrationtest/mapper/orm/src/test/java/org/hibernate/search/integrationtest/mapper/orm/massindexing/MassIndexingMonitorIT.java @@ -22,7 +22,7 @@ import org.hibernate.search.mapper.orm.automaticindexing.AutomaticIndexingStrategyName; import org.hibernate.search.mapper.orm.cfg.HibernateOrmMapperSettings; import org.hibernate.search.mapper.orm.massindexing.MassIndexer; -import org.hibernate.search.mapper.orm.massindexing.monitor.MassIndexingMonitor; +import org.hibernate.search.mapper.orm.massindexing.MassIndexingMonitor; import org.hibernate.search.mapper.orm.session.SearchSession; import org.hibernate.search.mapper.pojo.mapping.definition.annotation.GenericField; import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed; diff --git a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/logging/impl/Log.java b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/logging/impl/Log.java index aac1f9e0d2c..6d240c0e8ff 100644 --- a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/logging/impl/Log.java +++ b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/logging/impl/Log.java @@ -29,6 +29,7 @@ import org.hibernate.search.util.common.logging.impl.MessageConstants; import org.jboss.logging.BasicLogger; +import org.jboss.logging.Logger; import org.jboss.logging.annotations.Cause; import org.jboss.logging.annotations.FormatWith; import org.jboss.logging.annotations.LogMessage; @@ -255,4 +256,10 @@ SearchException invalidEntitySuperType(String entityName, + " 1) be mass-indexed or 2) set its document ID to a property that is not its entity ID." ) SearchException nonJpaEntityType(PojoRawTypeIdentifier typeIdentifier); + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = ID_OFFSET_2 + 31, + value = "The mass indexing failure handler threw an exception while handling a previous failure." + + " The failure may not have been reported.") + void failureInMassIndexingFailureHandler(@Cause Throwable t); } diff --git a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/MassIndexer.java b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/MassIndexer.java index 1408d7d193e..86522daecfc 100644 --- a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/MassIndexer.java +++ b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/MassIndexer.java @@ -9,7 +9,6 @@ import java.util.concurrent.CompletableFuture; import org.hibernate.CacheMode; -import org.hibernate.search.mapper.orm.massindexing.monitor.MassIndexingMonitor; /** * A MassIndexer is useful to rebuild the indexes from the @@ -138,4 +137,15 @@ public interface MassIndexer { * @return {@code this} for method chaining */ MassIndexer monitor(MassIndexingMonitor monitor); + + /** + * Set the {@link MassIndexingFailureHandler}. + *

+ * The default handler just forwards failures to the + * {@link org.hibernate.search.engine.cfg.EngineSettings#BACKGROUND_FAILURE_HANDLER background failure handler}. + * + * @param failureHandler The handler for failures occurring during mass indexing. + * @return {@code this} for method chaining + */ + MassIndexer failureHandler(MassIndexingFailureHandler failureHandler); } diff --git a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/MassIndexingEntityFailureContext.java b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/MassIndexingEntityFailureContext.java new file mode 100644 index 00000000000..00790ed0f48 --- /dev/null +++ b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/MassIndexingEntityFailureContext.java @@ -0,0 +1,59 @@ +/* + * 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 . + */ +package org.hibernate.search.mapper.orm.massindexing; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Contextual information about a failure to load or index a entities during mass indexing. + */ +public class MassIndexingEntityFailureContext extends MassIndexingFailureContext { + + public static Builder builder() { + return new Builder(); + } + + private final List entityReferences; + + private MassIndexingEntityFailureContext(Builder builder) { + super( builder ); + this.entityReferences = builder.entityReferences == null + ? Collections.emptyList() : Collections.unmodifiableList( builder.entityReferences ); + } + + /** + * @return A list of references to entities that may not be indexed correctly as a result of the failure. + * Never {@code null}, but may be empty. + * Use {@link Object#toString()} to get a textual representation of each reference, + * or cast it to the mapper-specific {@code EntityReference} type. + */ + public List getEntityReferences() { + return entityReferences; + } + + public static class Builder extends MassIndexingFailureContext.Builder { + + private List entityReferences; + + private Builder() { + } + + public void entityReference(Object entityReference) { + if ( entityReferences == null ) { + entityReferences = new ArrayList<>(); + } + entityReferences.add( entityReference ); + } + + @Override + public MassIndexingEntityFailureContext build() { + return new MassIndexingEntityFailureContext( this ); + } + } +} diff --git a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/MassIndexingFailureContext.java b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/MassIndexingFailureContext.java new file mode 100644 index 00000000000..ba3c1d86490 --- /dev/null +++ b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/MassIndexingFailureContext.java @@ -0,0 +1,83 @@ +/* + * 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 . + */ +package org.hibernate.search.mapper.orm.massindexing; + +import org.hibernate.search.util.common.AssertionFailure; + +/** + * Contextual information about a failing operation during mass indexing. + */ +public class MassIndexingFailureContext { + + /** + * @return A new {@link MassIndexingFailureContext} builder. + */ + public static Builder builder() { + return new Builder(); + } + + private final Throwable throwable; + + private final Object failingOperation; + + MassIndexingFailureContext(Builder builder) { + /* + * Avoid nulls: they should not happen, and they are most likely bugs in Hibernate Search, + * but we don't want user-implemented failure handlers to fail because of that + * (they would throw an NPE which may produce disastrous results such as killing background threads). + */ + this.throwable = builder.throwable == null + ? new AssertionFailure( + "Unknown throwable: missing throwable when reporting the failure." + + " There is probably a bug in Hibernate Search, please report it." + ) + : builder.throwable; + this.failingOperation = builder.failingOperation == null + ? "Unknown operation: missing operation when reporting the failure." + + " There is probably a bug in Hibernate Search, please report it." + : builder.failingOperation; + } + + /** + * @return The {@link Exception} or {@link Error} thrown when the operation failed. + * Never {@code null}. + */ + public Throwable getThrowable() { + return this.throwable; + } + + /** + * @return The operation that triggered the failure. + * Never {@code null}. + * Use {@link Object#toString()} to get a textual representation. + */ + public Object getFailingOperation() { + return this.failingOperation; + } + + public static class Builder { + + private Throwable throwable; + private Object failingOperation; + + Builder() { + } + + public void throwable(Throwable th) { + this.throwable = th; + } + + public void failingOperation(Object failingOperation) { + this.failingOperation = failingOperation; + } + + public MassIndexingFailureContext build() { + return new MassIndexingFailureContext( this ); + } + } + +} diff --git a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/MassIndexingFailureHandler.java b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/MassIndexingFailureHandler.java new file mode 100644 index 00000000000..2e495f37a3d --- /dev/null +++ b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/MassIndexingFailureHandler.java @@ -0,0 +1,50 @@ +/* + * 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 . + */ +package org.hibernate.search.mapper.orm.massindexing; + +/** + * A handler for failures occurring during mass indexing. + *

+ * The handler should be used to report failures to application maintainers. + * The default failure handler simply delegates to the configured {@link org.hibernate.search.engine.reporting.FailureHandler}, + * which by default logs failures at the {@code ERROR} level, + * but it can be replaced with a custom implementations + * by configuring the mass indexer. + *

+ * Handlers can be called from multiple threads simultaneously: implementations must be thread-safe. + */ +public interface MassIndexingFailureHandler { + + /** + * Handle a generic failure. + *

+ * This method is expected to report the failure somewhere (logs, ...), + * then return as quickly as possible. + * Heavy error processing (sending emails, ...), if any, should be done asynchronously. + *

+ * Any error or exception thrown by this method will be caught by Hibernate Search and logged. + * + * @param context Contextual information about the failure (throwable, operation, ...) + */ + void handle(MassIndexingFailureContext context); + + /** + * Handle a failure when indexing an entity. + *

+ * This method is expected to report the failure somewhere (logs, ...), + * then return as quickly as possible. + * Heavy error processing (sending emails, ...), if any, should be done asynchronously. + *

+ * Any error or exception thrown by this method will be caught by Hibernate Search and logged. + * + * @param context Contextual information about the failure (throwable, operation, ...) + */ + default void handle(MassIndexingEntityFailureContext context) { + handle( (MassIndexingFailureContext) context ); + } + +} diff --git a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/MassIndexingMonitor.java b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/MassIndexingMonitor.java new file mode 100644 index 00000000000..b47b0f4c12d --- /dev/null +++ b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/MassIndexingMonitor.java @@ -0,0 +1,100 @@ +/* + * 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 . + */ +package org.hibernate.search.mapper.orm.massindexing; + +/** + * A component that monitors progress of mass indexing. + *

+ * As a MassIndexer can take some time to finish its job, + * it is often necessary to monitor its progress. + * The default, built-in monitor logs progress periodically at the INFO level, + * but a custom monitor can be set by implementing this interface + * and passing an instance to {@link org.hibernate.search.mapper.orm.massindexing.MassIndexer#monitor(MassIndexingMonitor)}. + *

+ * Implementations must be threadsafe. + * + * @author Sanne Grinovero + * @author Hardy Ferentschik + */ +public interface MassIndexingMonitor { + + /** + * Notify the monitor that {@code increment} more documents have been added to the index. + *

+ * Summing the numbers passed to this method gives the total + * number of documents that have been added to the index so far. + *

+ * This method is invoked several times during indexing, + * and calls are incremental: + * calling {@code documentsAdded(3)} and then {@code documentsAdded(1)} + * should be understood as "3+1 documents, i.e. 4 documents have been added to the index". + *

+ * This method can be invoked from several threads thus implementors are required to be thread-safe. + * + * @param increment additional number of documents built + */ + void documentsAdded(long increment); + + /** + * Notify the monitor that {@code increment} more documents have been built. + *

+ * Summing the numbers passed to this method gives the total + * number of documents that have been built so far. + *

+ * This method is invoked several times during indexing, + * and calls are incremental: + * calling {@code documentsBuilt(3)} and then {@code documentsBuilt(1)} + * should be understood as "3+1 documents, i.e. 4 documents have been built". + *

+ * This method can be invoked from several threads thus implementors are required to be thread-safe. + * + * @param increment additional number of documents built + */ + void documentsBuilt(long increment); + + /** + * Notify the monitor that {@code increment} more entities have been loaded from the database. + *

+ * Summing the numbers passed to this method gives the total + * number of entities that have been loaded so far. + *

+ * This method is invoked several times during indexing, + * and calls are incremental: + * calling {@code entitiesLoaded(3)} and then {@code entitiesLoaded(1)} + * should be understood as "3+1 documents, i.e. 4 documents have been loaded". + *

+ * This method can be invoked from several threads thus implementors are required to be thread-safe. + * + * @param increment additional number of entities loaded from database + */ + void entitiesLoaded(long increment); + + /** + * Notify the monitor that {@code increment} more entities have been + * detected in the database and will be indexed. + *

+ * Summing the numbers passed to this method gives the total + * number of entities that Hibernate Search plans to index. + * This number can be incremented during indexing + * as Hibernate Search moves from one entity type to the next. + *

+ * This method is invoked several times during indexing, + * and calls are incremental: + * calling {@code addToTotalCount(3)} and then {@code addToTotalCount(1)} + * should be understood as "3+1 documents, i.e. 4 documents will be indexed". + *

+ * This method can be invoked from several threads thus implementors are required to be thread-safe. + * + * @param increment additional number of entities that will be indexed + */ + void addToTotalCount(long increment); + + /** + * Notify the monitor that indexing is complete. + */ + void indexingCompleted(); +} diff --git a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/impl/DelegatingMassIndexingFailureHandler.java b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/impl/DelegatingMassIndexingFailureHandler.java new file mode 100644 index 00000000000..f89b3e4ffe0 --- /dev/null +++ b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/impl/DelegatingMassIndexingFailureHandler.java @@ -0,0 +1,42 @@ +/* + * 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 . + */ +package org.hibernate.search.mapper.orm.massindexing.impl; + +import org.hibernate.search.engine.reporting.EntityIndexingFailureContext; +import org.hibernate.search.engine.reporting.FailureContext; +import org.hibernate.search.engine.reporting.FailureHandler; +import org.hibernate.search.mapper.orm.massindexing.MassIndexingEntityFailureContext; +import org.hibernate.search.mapper.orm.massindexing.MassIndexingFailureContext; +import org.hibernate.search.mapper.orm.massindexing.MassIndexingFailureHandler; + +public class DelegatingMassIndexingFailureHandler implements MassIndexingFailureHandler { + + private final FailureHandler delegate; + + public DelegatingMassIndexingFailureHandler(FailureHandler delegate) { + this.delegate = delegate; + } + + @Override + public void handle(MassIndexingFailureContext context) { + FailureContext.Builder builder = FailureContext.builder(); + builder.throwable( context.getThrowable() ); + builder.failingOperation( context.getFailingOperation() ); + delegate.handle( builder.build() ); + } + + @Override + public void handle(MassIndexingEntityFailureContext context) { + EntityIndexingFailureContext.Builder builder = EntityIndexingFailureContext.builder(); + builder.throwable( context.getThrowable() ); + builder.failingOperation( context.getFailingOperation() ); + for ( Object entityReference : context.getEntityReferences() ) { + builder.entityReference( entityReference ); + } + delegate.handle( builder.build() ); + } +} diff --git a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/impl/FailSafeMassIndexingFailureHandlerWrapper.java b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/impl/FailSafeMassIndexingFailureHandlerWrapper.java new file mode 100644 index 00000000000..055502d9eb5 --- /dev/null +++ b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/impl/FailSafeMassIndexingFailureHandlerWrapper.java @@ -0,0 +1,47 @@ +/* + * 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 . + */ +package org.hibernate.search.mapper.orm.massindexing.impl; + +import java.lang.invoke.MethodHandles; + +import org.hibernate.search.mapper.orm.logging.impl.Log; +import org.hibernate.search.mapper.orm.massindexing.MassIndexingEntityFailureContext; +import org.hibernate.search.mapper.orm.massindexing.MassIndexingFailureContext; +import org.hibernate.search.mapper.orm.massindexing.MassIndexingFailureHandler; +import org.hibernate.search.util.common.logging.impl.LoggerFactory; + +public class FailSafeMassIndexingFailureHandlerWrapper implements MassIndexingFailureHandler { + + private static final Log log = LoggerFactory.make( Log.class, MethodHandles.lookup() ); + + private final MassIndexingFailureHandler delegate; + + public FailSafeMassIndexingFailureHandlerWrapper(MassIndexingFailureHandler delegate) { + this.delegate = delegate; + } + + @Override + public void handle(MassIndexingFailureContext context) { + try { + delegate.handle( context ); + } + catch (Throwable t) { + log.failureInMassIndexingFailureHandler( t ); + } + } + + @Override + public void handle(MassIndexingEntityFailureContext context) { + try { + delegate.handle( context ); + } + catch (Throwable t) { + log.failureInMassIndexingFailureHandler( t ); + } + } + +} diff --git a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/monitor/impl/SimpleIndexingProgressMonitor.java b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/impl/LoggingMassIndexingMonitor.java similarity index 89% rename from mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/monitor/impl/SimpleIndexingProgressMonitor.java rename to mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/impl/LoggingMassIndexingMonitor.java index a0394af5e52..0567eda2eb3 100644 --- a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/monitor/impl/SimpleIndexingProgressMonitor.java +++ b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/impl/LoggingMassIndexingMonitor.java @@ -4,7 +4,7 @@ * License: GNU Lesser General Public License (LGPL), version 2.1 or later * See the lgpl.txt file in the root directory or . */ -package org.hibernate.search.mapper.orm.massindexing.monitor.impl; +package org.hibernate.search.mapper.orm.massindexing.impl; import java.lang.invoke.MethodHandles; import java.util.concurrent.TimeUnit; @@ -12,7 +12,7 @@ import java.util.concurrent.atomic.LongAdder; import org.hibernate.search.mapper.orm.logging.impl.Log; -import org.hibernate.search.mapper.orm.massindexing.monitor.MassIndexingMonitor; +import org.hibernate.search.mapper.orm.massindexing.MassIndexingMonitor; import org.hibernate.search.util.common.logging.impl.LoggerFactory; /** @@ -21,7 +21,7 @@ * * @author Sanne Grinovero */ -public class SimpleIndexingProgressMonitor implements MassIndexingMonitor { +public class LoggingMassIndexingMonitor implements MassIndexingMonitor { private static final Log log = LoggerFactory.make( Log.class, MethodHandles.lookup() ); private final AtomicLong documentsDoneCounter = new AtomicLong(); @@ -32,7 +32,7 @@ public class SimpleIndexingProgressMonitor implements MassIndexingMonitor { /** * Logs progress of indexing job every 50 documents written. */ - public SimpleIndexingProgressMonitor() { + public LoggingMassIndexingMonitor() { this( 50 ); } @@ -42,7 +42,7 @@ public SimpleIndexingProgressMonitor() { * * @param logAfterNumberOfDocuments log each time the specified number of documents has been added */ - public SimpleIndexingProgressMonitor(int logAfterNumberOfDocuments) { + public LoggingMassIndexingMonitor(int logAfterNumberOfDocuments) { this.logAfterNumberOfDocuments = logAfterNumberOfDocuments; } diff --git a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/impl/MassIndexerImpl.java b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/impl/MassIndexerImpl.java index eea6cd69eb3..6f890e552ee 100644 --- a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/impl/MassIndexerImpl.java +++ b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/impl/MassIndexerImpl.java @@ -19,8 +19,8 @@ import org.hibernate.search.mapper.orm.common.impl.HibernateOrmUtils; import org.hibernate.search.mapper.orm.massindexing.MassIndexer; import org.hibernate.search.mapper.orm.logging.impl.Log; -import org.hibernate.search.mapper.orm.massindexing.monitor.MassIndexingMonitor; -import org.hibernate.search.mapper.orm.massindexing.monitor.impl.SimpleIndexingProgressMonitor; +import org.hibernate.search.mapper.orm.massindexing.MassIndexingFailureHandler; +import org.hibernate.search.mapper.orm.massindexing.MassIndexingMonitor; import org.hibernate.search.mapper.pojo.work.spi.PojoScopeWorkspace; import org.hibernate.search.util.common.impl.Futures; import org.hibernate.search.util.common.logging.impl.LoggerFactory; @@ -54,10 +54,12 @@ public class MassIndexerImpl implements MassIndexer { private boolean mergeSegmentsOnFinish = false; private boolean purgeAtStart = true; private boolean mergeSegmentsAfterPurge = true; - private MassIndexingMonitor monitor; private int idFetchSize = 100; //reasonable default as we only load IDs private Integer idLoadingTransactionTimeout; + private MassIndexingFailureHandler failureHandler; + private MassIndexingMonitor monitor; + public MassIndexerImpl(HibernateOrmMassIndexingMappingContext mappingContext, Set> targetedIndexedTypes, DetachedBackendSessionContext sessionContext, @@ -189,7 +191,7 @@ public void startAndWait() throws InterruptedException { protected BatchCoordinator createCoordinator() { MassIndexingNotifier notifier = new MassIndexingNotifier( - mappingContext.getFailureHandler(), + getOrCreateFailureHandler(), getOrCreateMonitor() ); return new BatchCoordinator( @@ -217,12 +219,27 @@ public MassIndexer idFetchSize(int idFetchSize) { return this; } + @Override + public MassIndexer failureHandler(MassIndexingFailureHandler failureHandler) { + this.failureHandler = failureHandler; + return this; + } + + private MassIndexingFailureHandler getOrCreateFailureHandler() { + MassIndexingFailureHandler result = failureHandler; + if ( result == null ) { + result = new DelegatingMassIndexingFailureHandler( mappingContext.getFailureHandler() ); + } + result = new FailSafeMassIndexingFailureHandlerWrapper( result ); + return result; + } + private MassIndexingMonitor getOrCreateMonitor() { if ( monitor != null ) { return monitor; } // TODO HSEARCH-3057 use a JMX monitor if JMX is enabled (see Search 5) - return new SimpleIndexingProgressMonitor(); + return new LoggingMassIndexingMonitor(); } } diff --git a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/impl/MassIndexingNotifier.java b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/impl/MassIndexingNotifier.java index 7c4ea8c1341..3d924e4776d 100644 --- a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/impl/MassIndexingNotifier.java +++ b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/impl/MassIndexingNotifier.java @@ -11,13 +11,13 @@ import java.util.concurrent.atomic.LongAdder; import org.hibernate.Session; -import org.hibernate.search.engine.reporting.EntityIndexingFailureContext; -import org.hibernate.search.engine.reporting.FailureContext; -import org.hibernate.search.engine.reporting.FailureHandler; import org.hibernate.search.mapper.orm.common.EntityReference; import org.hibernate.search.mapper.orm.common.impl.EntityReferenceImpl; import org.hibernate.search.mapper.orm.logging.impl.Log; -import org.hibernate.search.mapper.orm.massindexing.monitor.MassIndexingMonitor; +import org.hibernate.search.mapper.orm.massindexing.MassIndexingEntityFailureContext; +import org.hibernate.search.mapper.orm.massindexing.MassIndexingFailureContext; +import org.hibernate.search.mapper.orm.massindexing.MassIndexingFailureHandler; +import org.hibernate.search.mapper.orm.massindexing.MassIndexingMonitor; import org.hibernate.search.util.common.SearchException; import org.hibernate.search.util.common.logging.impl.LoggerFactory; @@ -25,15 +25,14 @@ class MassIndexingNotifier { private static final Log log = LoggerFactory.make( Log.class, MethodHandles.lookup() ); - private final FailureHandler failureHandler; + private final MassIndexingFailureHandler failureHandler; private final MassIndexingMonitor monitor; private final AtomicReference entityIndexingFirstFailure = new AtomicReference<>( null ); private final LongAdder entityIndexingFailureCount = new LongAdder(); - MassIndexingNotifier(FailureHandler failureHandler, - MassIndexingMonitor monitor) { + MassIndexingNotifier(MassIndexingFailureHandler failureHandler, MassIndexingMonitor monitor) { this.failureHandler = failureHandler; this.monitor = monitor; } @@ -43,7 +42,7 @@ void notifyAddedTotalCount(long totalCount) { } void notifyRunnableFailure(Exception exception, String operation) { - FailureContext.Builder contextBuilder = FailureContext.builder(); + MassIndexingFailureContext.Builder contextBuilder = MassIndexingFailureContext.builder(); contextBuilder.throwable( exception ); contextBuilder.failingOperation( operation ); failureHandler.handle( contextBuilder.build() ); @@ -67,7 +66,7 @@ void notifyEntityIndexingFailure(HibernateOrmMassIndexingIndexedTypeContext< entityIndexingFirstFailure.compareAndSet( null, recordedFailure ); entityIndexingFailureCount.increment(); - EntityIndexingFailureContext.Builder contextBuilder = EntityIndexingFailureContext.builder(); + MassIndexingEntityFailureContext.Builder contextBuilder = MassIndexingEntityFailureContext.builder(); contextBuilder.throwable( throwable ); // Add minimal information here, but information we're sure we can get contextBuilder.failingOperation( log.massIndexerIndexingInstance( type.getJpaEntityName() ) ); @@ -104,7 +103,7 @@ void notifyIndexingCompletedWithFailure(Throwable throwable) { throwable.addSuppressed( entityIndexingException ); } - FailureContext.Builder contextBuilder = FailureContext.builder(); + MassIndexingFailureContext.Builder contextBuilder = MassIndexingFailureContext.builder(); contextBuilder.throwable( throwable ); contextBuilder.failingOperation( log.massIndexerOperation() ); failureHandler.handle( contextBuilder.build() ); diff --git a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/monitor/MassIndexingMonitor.java b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/monitor/MassIndexingMonitor.java index 459d1643e81..b1f482638e8 100644 --- a/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/monitor/MassIndexingMonitor.java +++ b/mapper/orm/src/main/java/org/hibernate/search/mapper/orm/massindexing/monitor/MassIndexingMonitor.java @@ -7,94 +7,8 @@ package org.hibernate.search.mapper.orm.massindexing.monitor; /** - * A component that monitors progress of mass indexing. - *

- * As a MassIndexer can take some time to finish its job, - * it is often necessary to monitor its progress. - * The default, built-in monitor logs progress periodically at the INFO level, - * but a custom monitor can be set by implementing this interface - * and passing an instance to {@link org.hibernate.search.mapper.orm.massindexing.MassIndexer#monitor(MassIndexingMonitor)}. - *

- * Implementations must be threadsafe. - * - * @author Sanne Grinovero - * @author Hardy Ferentschik + * @deprecated Implement {@link org.hibernate.search.mapper.orm.massindexing.MassIndexingMonitor} instead. */ -public interface MassIndexingMonitor { - - /** - * Notify the monitor that {@code increment} more documents have been added to the index. - *

- * Summing the numbers passed to this method gives the total - * number of documents that have been added to the index so far. - *

- * This method is invoked several times during indexing, - * and calls are incremental: - * calling {@code documentsAdded(3)} and then {@code documentsAdded(1)} - * should be understood as "3+1 documents, i.e. 4 documents have been added to the index". - *

- * This method can be invoked from several threads thus implementors are required to be thread-safe. - * - * @param increment additional number of documents built - */ - void documentsAdded(long increment); - - /** - * Notify the monitor that {@code increment} more documents have been built. - *

- * Summing the numbers passed to this method gives the total - * number of documents that have been built so far. - *

- * This method is invoked several times during indexing, - * and calls are incremental: - * calling {@code documentsBuilt(3)} and then {@code documentsBuilt(1)} - * should be understood as "3+1 documents, i.e. 4 documents have been built". - *

- * This method can be invoked from several threads thus implementors are required to be thread-safe. - * - * @param increment additional number of documents built - */ - void documentsBuilt(long increment); - - /** - * Notify the monitor that {@code increment} more entities have been loaded from the database. - *

- * Summing the numbers passed to this method gives the total - * number of entities that have been loaded so far. - *

- * This method is invoked several times during indexing, - * and calls are incremental: - * calling {@code entitiesLoaded(3)} and then {@code entitiesLoaded(1)} - * should be understood as "3+1 documents, i.e. 4 documents have been loaded". - *

- * This method can be invoked from several threads thus implementors are required to be thread-safe. - * - * @param increment additional number of entities loaded from database - */ - void entitiesLoaded(long increment); - - /** - * Notify the monitor that {@code increment} more entities have been - * detected in the database and will be indexed. - *

- * Summing the numbers passed to this method gives the total - * number of entities that Hibernate Search plans to index. - * This number can be incremented during indexing - * as Hibernate Search moves from one entity type to the next. - *

- * This method is invoked several times during indexing, - * and calls are incremental: - * calling {@code addToTotalCount(3)} and then {@code addToTotalCount(1)} - * should be understood as "3+1 documents, i.e. 4 documents will be indexed". - *

- * This method can be invoked from several threads thus implementors are required to be thread-safe. - * - * @param increment additional number of entities that will be indexed - */ - void addToTotalCount(long increment); - - /** - * Notify the monitor that indexing is complete. - */ - void indexingCompleted(); +@Deprecated +public interface MassIndexingMonitor extends org.hibernate.search.mapper.orm.massindexing.MassIndexingMonitor { }