package fr.openwide.core.jpa.batch.executor; import java.util.Collection; import java.util.List; import java.util.concurrent.ExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionException; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.SimpleTransactionStatus; import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionOperations; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import fr.openwide.core.commons.util.functional.Joiners; import fr.openwide.core.jpa.batch.runnable.IBatchRunnable; import fr.openwide.core.jpa.batch.runnable.Writeability; import fr.openwide.core.jpa.batch.util.IBeforeClearListener; import fr.openwide.core.jpa.business.generic.model.GenericEntity; import fr.openwide.core.jpa.business.generic.model.GenericEntityCollectionReference; import fr.openwide.core.jpa.exception.ServiceException; import fr.openwide.core.jpa.query.IQuery; import fr.openwide.core.jpa.query.Queries; @Component @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public class SimpleHibernateBatchExecutor extends AbstractBatchExecutor<SimpleHibernateBatchExecutor> { private static final Logger LOGGER = LoggerFactory.getLogger(SimpleHibernateBatchExecutor.class); @Autowired(required = false) private Collection<IBeforeClearListener> clearListeners = ImmutableList.of(); private ExecutionStrategyFactory executionStrategyFactory = ExecutionStrategyFactory.COMMIT_ON_END; private List<Class<?>> classesToReindex = Lists.newArrayListWithCapacity(0); /** * Use one transaction for the whole execution, periodically flushing the Hibernate session and the Hibernate Search * indexes. * <p>This is the default. * <p>If an error occurs, this strategy will leave the database unchanged (with no modification), but <strong>will * leave Hibernate Search indexes half-changed</strong> (with only the changes of the successfully executed * previous steps). * <p>Thus this strategy is fully transactional, but may <strong>corrupt your indexes</strong> on error, leaving * them out-of-sync with your database. This requires particular caution when handling errors (for instance full * reindex). */ public SimpleHibernateBatchExecutor commitOnEnd() { this.executionStrategyFactory = ExecutionStrategyFactory.COMMIT_ON_END; return this; } /** * Use one transaction for each step of the execution: * <ul> * <li>{@link IBatchRunnable#preExecute()}</li> * <li>each {@link IBatchRunnable#executePartition(List)} * <li>{@link IBatchRunnable#postExecute()}</li> * </ul> * <p>If an error occurs, this strategy will leave the database and the Hibernate Search indices half-changed * (with only the changes of the successfully executed previous steps). * <p>Thus this strategy <strong>is not fully transactional</strong>, but will ensure your indexes stay in sync * with your database on error. */ public SimpleHibernateBatchExecutor commitOnStep() { this.executionStrategyFactory = ExecutionStrategyFactory.COMMIT_ON_STEP; return this; } /** * Triggers a full reindex of the given classes at the end of the execution. * <p>Mostly useful when using {@link #commitOnEnd()} transaction strategy. */ public SimpleHibernateBatchExecutor reindexClasses(Class<?> clazz, Class<?>... classes) { classesToReindex = Lists.asList(clazz, classes); return this; } /** * Runs a batch execution against a given list of entities (given by their IDs). */ public <E extends GenericEntity<Long, ?>> void run(final Class<E> clazz, final List<Long> entityIds, final IBatchRunnable<E> batchRunnable) { GenericEntityCollectionReference<Long, E> reference = new GenericEntityCollectionReference<>(clazz, entityIds); runNonConsuming(String.format("class %s", clazz), entityService.getQuery(reference), batchRunnable); } /** * Runs a batch execution against a {@link IQuery query}'s result, <strong>expecting that the execution * won't change the query's results</strong>. * <p>This last requirement allows us to safely break down the execution this way: * first execute on <code>query.list(0, 100)</code>, then (if there was a result) <code>query.list(100, 100)</code>, * and so on. * <p>The <code>query</code> may be: * <ul> * <li>Your own implementation (of IQuery, or more particularly of ISearchQuery) * <li>Retrieved from a DAO that used {@link Queries#fromQueryDsl(com.querydsl.core.support.FetchableQueryBase)} to * adapt a QueryDSL query * </ul> */ public <E extends GenericEntity<Long, ?>> void runNonConsuming(String loggerContext, final IQuery<E> query, final IBatchRunnable<E> batchRunnable) { doRun(loggerContext, query, false, batchRunnable); } /** * Runs a batch execution against a {@link IQuery query}'s result, <strong>expecting that the execution * will remove all processed elements from the query's results</strong>. * <p>This last requirement allows us to safely break down the execution this way: * first execute on <code>query.list(0, 100)</code>, then (if there was a result) execute on * <code>query.list(0, 100)</code> <strong>again</strong>, and so on. * <p>The <code>query</code> may be: * <ul> * <li>Your own implementation (of IQuery, or more particularly of ISearchQuery) * <li>Retrieved from a DAO that used {@link Queries#fromQueryDsl(com.querydsl.core.support.FetchableQueryBase)} to * adapt a QueryDSL query * </ul> */ public <E extends GenericEntity<Long, ?>> void runConsuming(String loggerContext, final IQuery<E> query, final IBatchRunnable<E> batchRunnable) { Preconditions.checkArgument( Writeability.READ_WRITE.equals(batchRunnable.getWriteability()), "runConsuming() must be used with a read/write runnable, but %s is read-only", batchRunnable ); doRun(loggerContext, query, true, batchRunnable); } private <E extends GenericEntity<Long, ?>> void doRun(final String loggerContext, final IQuery<E> query, final boolean consuming, final IBatchRunnable<E> batchRunnable) { final ExecutionStrategy<E> executionStrategy = executionStrategyFactory.create(this, query, batchRunnable); executionStrategy.getExecuteTransactionOperations().execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { doRun(executionStrategy, loggerContext, consuming); } }); } private <E extends GenericEntity<Long, ?>> void doRun(ExecutionStrategy<E> executionStrategy, String loggerContext, boolean consuming) { long expectedTotalCount = executionStrategy.getExpectedTotalCount(); long offset = 0; LOGGER.info("Beginning batch for %1$s: %2$d objects", loggerContext, expectedTotalCount); try { LOGGER.info(" preExecute start"); try { executionStrategy.preExecute(); } catch (RuntimeException e) { throw new ExecutionException("Exception on preExecute", e); } LOGGER.info(" preExecute end"); LOGGER.info(" starting batch executions"); try { int partitionSize; do { partitionSize = executionStrategy.executePartition( // Don't use the offset if the runnable is consuming the query's results consuming ? 0 : offset, batchSize ); offset += partitionSize; LOGGER.info(" treated %1$d/%2$d objects", offset, expectedTotalCount); } while (partitionSize > 0); } catch (RuntimeException e) { throw new ExecutionException("Exception on executePartition", e); } LOGGER.info(" end of batch executions"); LOGGER.info(" postExecute start"); try { executionStrategy.postExecute(); } catch (RuntimeException e) { throw new ExecutionException("Exception on postExecute", e); } LOGGER.info(" postExecute end"); if (classesToReindex.size() > 0) { LOGGER.info(" reindexing classes %1$s", Joiners.onComma().join(classesToReindex)); try { hibernateSearchService.reindexClasses(classesToReindex); } catch (ServiceException e) { LOGGER.error(" reindexing failure", e); } LOGGER.info(" end of reindexing"); } LOGGER.info("End of batch for %1$s: %2$d/%3$d objects treated", loggerContext, offset, expectedTotalCount); } catch (ExecutionException e) { LOGGER.info("End of batch for %1$s: %2$d/%3$d objects treated, but caught exception '%s'", loggerContext, offset, expectedTotalCount, e); try { LOGGER.info(" onError start"); executionStrategy.onError(e); LOGGER.info(" onError end (exception was NOT propagated)"); } catch (RuntimeException e2) { LOGGER.info(" onError end (exception WAS propagated)"); throw e2; } } } protected <E extends GenericEntity<Long, ?>> List<E> listEntitiesByIds(Class<E> clazz, Collection<Long> entityIds) { return entityService.listEntity(clazz, entityIds); } @Override protected SimpleHibernateBatchExecutor thisAsT() { return this; } private enum ExecutionStrategyFactory { COMMIT_ON_STEP { @Override <T> ExecutionStrategy<T> create(SimpleHibernateBatchExecutor executor, IQuery<T> query, IBatchRunnable<T> runnable) { return new CommitOnStepExecutionStrategy<T>(executor, query, runnable); } }, COMMIT_ON_END { @Override <T> ExecutionStrategy<T> create(SimpleHibernateBatchExecutor executor, IQuery<T> query, IBatchRunnable<T> runnable) { return new CommitOnEndExecutionStrategy<T>(executor, query, runnable); } }; abstract <T> ExecutionStrategy<T> create(SimpleHibernateBatchExecutor executor, IQuery<T> query, IBatchRunnable<T> runnable); } private abstract static class ExecutionStrategy<T> { protected final SimpleHibernateBatchExecutor executor; protected final IQuery<T> query; protected final IBatchRunnable<T> runnable; public ExecutionStrategy(SimpleHibernateBatchExecutor executor, IQuery<T> query, IBatchRunnable<T> runnable) { super(); this.executor = executor; this.query = query; this.runnable = runnable; } public abstract TransactionOperations getExecuteTransactionOperations(); public abstract long getExpectedTotalCount(); public abstract void preExecute(); /** * @param offset * @param limit * @return The number of items in the partition. */ public abstract int executePartition(long offset, long limit); public abstract void postExecute(); public void onError(ExecutionException e) { runnable.onError(e); } } private final static class CommitOnEndExecutionStrategy<T> extends ExecutionStrategy<T> { private final boolean isReadOnly = Writeability.READ_ONLY.equals(runnable.getWriteability()); private CommitOnEndExecutionStrategy(SimpleHibernateBatchExecutor executor, IQuery<T> query, IBatchRunnable<T> runnable) { super(executor, query, runnable); } @Override public TransactionOperations getExecuteTransactionOperations() { return executor.newTransactionTemplate( runnable.getWriteability(), TransactionDefinition.PROPAGATION_REQUIRED ); } @Override public long getExpectedTotalCount() { return query.count(); } private void afterStep() { if (!isReadOnly) { executor.entityService.flush(); } for (IBeforeClearListener beforeClearListener : executor.clearListeners) { beforeClearListener.beforeClear(); } if (!isReadOnly) { executor.hibernateSearchService.flushToIndexes(); } executor.entityService.clear(); } @Override public void preExecute() { runnable.preExecute(); afterStep(); } @Override public int executePartition(long offset, long limit) { final List<T> partition = query.list(offset, limit); int partitionSize = partition.size(); if (partitionSize > 0) { runnable.executePartition(partition); afterStep(); } return partitionSize; } @Override public void postExecute() { runnable.postExecute(); afterStep(); } } private final static class CommitOnStepExecutionStrategy<T> extends ExecutionStrategy<T> { private final TransactionOperations stepTransactionTemplate = executor.newTransactionTemplate( runnable.getWriteability(), TransactionDefinition.PROPAGATION_REQUIRES_NEW ); private CommitOnStepExecutionStrategy(SimpleHibernateBatchExecutor executor, IQuery<T> query, IBatchRunnable<T> runnable) { super(executor, query, runnable); } @Override public TransactionOperations getExecuteTransactionOperations() { return new TransactionOperations() { @Override public <T2> T2 execute(TransactionCallback<T2> action) throws TransactionException { return action.doInTransaction(new SimpleTransactionStatus()); } }; } @Override public long getExpectedTotalCount() { return stepTransactionTemplate.execute(new TransactionCallback<Long>() { @Override public Long doInTransaction(TransactionStatus status) { return query.count(); } }); } @Override public void preExecute() { stepTransactionTemplate.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { runnable.preExecute(); } }); } @Override public int executePartition(final long offset, final long limit) { return stepTransactionTemplate.execute(new TransactionCallback<Integer>() { @Override public Integer doInTransaction(TransactionStatus status) { final List<T> partition = query.list(offset, limit); int partitionSize = partition.size(); if (partitionSize > 0) { runnable.executePartition(partition); } return partitionSize; } }); } @Override public void postExecute() { stepTransactionTemplate.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { runnable.postExecute(); } }); } } }