package fr.openwide.core.jpa.batch.processor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.support.TransactionOperations;
import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import fr.openwide.core.jpa.batch.monitor.ProcessorMonitorContext;
import fr.openwide.core.jpa.batch.monitor.ThreadLocalInitializingCallable;
import fr.openwide.core.jpa.batch.util.TransactionWrapperCallable;
import fr.openwide.core.spring.util.StringUtils;
public class ThreadedProcessor {
private static final Logger LOGGER = LoggerFactory.getLogger(ThreadedProcessor.class);
private static final RejectedExecutionHandler THROW_EXCEPTION_ON_REJECTED_EXECUTION = new ThreadPoolExecutor.AbortPolicy();
private static final Function<Runnable, Callable<Object>> RUNNABLE_TO_CALLABLE = new Function<Runnable, Callable<Object>>() {
@Override
public java.util.concurrent.Callable<Object> apply(Runnable input) {
return input == null ? null : Executors.callable(input);
}
};
private final int threadPoolSize;
private final int keepAliveTime;
private final TimeUnit keepAliveTimeUnit;
private final int maxTotalDuration;
private final TimeUnit maxTotalDurationTimeUnit;
private final boolean abortAllOnError;
private final Integer loggingCheckIntervalTime;
private final TimeUnit loggingCheckIntervalTimeUnit;
private final Integer maxLoggingTime;
private final TimeUnit maxLoggingTimeUnit;
private final Integer maxLoggingIncrement;
private final Logger progressLogger;
private ProcessorMonitorContext monitorContext;
public ThreadedProcessor(int threadPoolSize,
int maxTotalDuration, TimeUnit maxTotalDurationUnit,
int keepAliveTime, TimeUnit keepAliveTimeUnit,
boolean abortAllOnError) {
this(threadPoolSize, maxTotalDuration, maxTotalDurationUnit, keepAliveTime, keepAliveTimeUnit, abortAllOnError,
null, null, null, null, null, null);
}
public ThreadedProcessor(int threadPoolSize,
int maxTotalDuration, TimeUnit maxTotalDurationUnit,
int keepAliveTime, TimeUnit keepAliveTimeUnit,
boolean abortAllOnError,
Integer loggingCheckIntervalTime, TimeUnit loggingCheckIntervalTimeUnit,
Integer maxLoggingTime, TimeUnit maxLoggingTimeUnit,
Integer maxLoggingIncrement,
Logger progressLogger) {
super();
this.threadPoolSize = threadPoolSize;
this.keepAliveTime = keepAliveTime;
this.keepAliveTimeUnit = keepAliveTimeUnit;
this.abortAllOnError = abortAllOnError;
this.maxTotalDuration = maxTotalDuration;
this.maxTotalDurationTimeUnit = maxTotalDurationUnit;
this.loggingCheckIntervalTime = loggingCheckIntervalTime;
this.loggingCheckIntervalTimeUnit = loggingCheckIntervalTimeUnit;
this.maxLoggingTime = maxLoggingTime;
this.maxLoggingTimeUnit = maxLoggingTimeUnit;
this.maxLoggingIncrement = maxLoggingIncrement;
this.progressLogger = progressLogger;
}
public <T> void runWithoutTransaction(String loggerContext, Collection<? extends Runnable> runnables)
throws ExecutionException {
runWithoutTransaction(loggerContext, runnables, null);
}
public <T> void runWithoutTransaction(String loggerContext, Collection<? extends Runnable> runnables,
Integer totalItems) throws ExecutionException {
runWithTransaction(loggerContext, runnables, null, totalItems);
}
public <T> void runWithTransaction(final String loggerContext, Collection<? extends Runnable> runnables,
TransactionOperations TransactionOperations, Integer totalItems) throws ExecutionException {
callWithTransaction(loggerContext, Collections2.transform(runnables, RUNNABLE_TO_CALLABLE),
TransactionOperations, totalItems);
}
public <T> List<T> callWithoutTransaction(String loggerContext, Collection<? extends Callable<T>> callables)
throws ExecutionException {
return callWithoutTransaction(loggerContext, callables, null);
}
public <T> List<T> callWithoutTransaction(String loggerContext, Collection<? extends Callable<T>> callables,
Integer totalItems) throws ExecutionException {
return callWithTransaction(loggerContext, callables, null, totalItems);
}
public <T> List<T> callWithTransaction(final String loggerContext, Collection<? extends Callable<T>> callables,
TransactionOperations TransactionOperations, Integer totalItems) throws ExecutionException {
List<ListenableFuture<T>> futures = Lists.newArrayList();
// ThreadedPoolExecutor cannot be reused after shutdown, so we must instantiate it on each call.
BlockingQueue<Runnable> workingQueue = new LinkedBlockingQueue<Runnable>();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
threadPoolSize, threadPoolSize, keepAliveTime, keepAliveTimeUnit,
workingQueue, THROW_EXCEPTION_ON_REJECTED_EXECUTION);
ListeningExecutorService executor = MoreExecutors.listeningDecorator(threadPoolExecutor);
threadPoolExecutor.prestartAllCoreThreads();
Thread loggingThread = null;
this.monitorContext = new ProcessorMonitorContext();
if (progressLogger != null) {
loggingThread = new Thread(new LoggingRunnable(loggerContext));
}
if (totalItems != null) {
this.monitorContext.getTotalItems().set(totalItems);
}
try {
if (loggingThread != null) {
loggingThread.start();
}
try {
List<FutureCallback<? super T>> callbacks = Lists.newArrayList();
if (abortAllOnError) {
callbacks.add(new ThreadPoolExecutorAbortFutureCallback(threadPoolExecutor, loggerContext));
}
for (Callable<T> callable : callables) {
Callable<T> wrappedCallable = new ThreadLocalInitializingCallable<>(callable,
ProcessorMonitorContext.getThreadLocal(), monitorContext);
if (TransactionOperations != null) {
// All executions are wrapped in a separate transaction
// (if the template was provided)
wrappedCallable = new TransactionWrapperCallable<>(TransactionOperations, wrappedCallable);
}
ListenableFuture<T> future = executor.submit(wrappedCallable);
for (FutureCallback<? super T> callback : callbacks) {
Futures.addCallback(future, callback);
}
futures.add(future);
}
} catch (RejectedExecutionException e) {
LOGGER.error(
"{} - Some tasks could not be submitted to the ThreadPoolExecutor."
+ " Submitted tasks: {} of {}."
+ " This is probably due to an error in another task causing the ThreadPoolExecutor to shut down.",
loggerContext, futures.size(), callables.size());
}
executor.shutdown();
List<T> results = Lists.newArrayList();
List<ExecutionException> executionExceptions = Lists.newArrayList();
boolean interrupted = false;
try {
boolean terminated = executor.awaitTermination(maxTotalDuration, maxTotalDurationTimeUnit);
if (!terminated) {
LOGGER.error("{} - Tasks haven't terminated before the timeout of {} {}", maxTotalDuration,
maxTotalDurationTimeUnit.name());
}
LOGGER.info("{} - {} elements treated", loggerContext, this.monitorContext.getDoneItems());
if (this.monitorContext.getFailedItems().get() > 0) {
LOGGER.error("{} - {} elements in error", loggerContext,
this.monitorContext.getFailedItems().get());
}
if (this.monitorContext.getIgnoredItems().get() > 0) {
LOGGER.info("{} - {} elements ignored", loggerContext, this.monitorContext.getIgnoredItems().get());
}
for (Future<T> future : futures) {
try {
T result = future.get(0, TimeUnit.SECONDS);
results.add(result);
} catch (ExecutionException e) {
executionExceptions.add(e);
} catch (CancellationException|TimeoutException timeoutException) {
// already notified by a warn
future.cancel(true);
interrupted = true;
}
}
} catch (InterruptedException e) {
throw new IllegalStateException("Batch error: timeout.", e);
}
if (!executionExceptions.isEmpty()) {
Iterator<ExecutionException> it = executionExceptions.iterator();
ExecutionException e = new ExecutionException(it.next().getCause());
while (it.hasNext()) {
e.addSuppressed(it.next().getCause());
}
throw e;
}
if (interrupted) {
throw new IllegalStateException("Batch error: a work thread was interrupted.");
}
return results;
} finally {
ProcessorMonitorContext.unset();
if (loggingThread != null) {
loggingThread.interrupt();
try {
loggingThread.join();
} catch (InterruptedException e) {
LOGGER.warn("{} - Thread interrupted while waiting for the end of the logging thread execution.",
loggerContext);
}
}
}
}
protected class LoggingRunnable implements Runnable {
private final String loggerContext;
private long startTime;
private long lastLoggingTime;
private int lastDoneItems;
public LoggingRunnable(String loggerContext) {
super();
this.loggerContext = loggerContext;
}
@Override
public void run() {
startTime = System.currentTimeMillis();
lastLoggingTime = startTime;
lastDoneItems = 0;
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(loggingCheckIntervalTimeUnit.toMillis(loggingCheckIntervalTime));
log(false);
} catch (InterruptedException e) {
log(true);
LOGGER.info("Logging thread interrupted.");
return;
}
}
log(true);
}
public void log(boolean force) {
if (progressLogger.isInfoEnabled()) {
long currentTime = System.currentTimeMillis();
int totalItems = monitorContext.getTotalItems().get();
int doneItems = monitorContext.getDoneItems().get();
int ignoredItems = monitorContext.getIgnoredItems().get();
if (force || (currentTime - lastLoggingTime) > maxLoggingTimeUnit.toMillis(maxLoggingTime)
|| (doneItems - lastDoneItems) > maxLoggingIncrement) {
Float speedSinceStart = (float) doneItems / (float) (currentTime - startTime);
int roundedSpeedSinceStart = Math.round(speedSinceStart * 1000);
Float speedSinceLast = (float) (doneItems - lastDoneItems) / (float) (currentTime - lastLoggingTime);
int roundedSpeedSinceLast = Math.round(speedSinceLast * 1000);
lastLoggingTime = currentTime;
lastDoneItems = doneItems;
StringBuilder sb = new StringBuilder();
if (StringUtils.hasText(loggerContext)) {
sb.append(loggerContext).append(" - ");
}
sb.append("In progress: {} / {} ({} ignored, {} items/s since start, {} items/s since last log)");
progressLogger.info(sb.toString(), doneItems, totalItems - ignoredItems, ignoredItems,
roundedSpeedSinceStart, roundedSpeedSinceLast);
}
}
if (progressLogger.isDebugEnabled()) {
StringBuilder sb = new StringBuilder();
if (StringUtils.hasText(loggerContext)) {
sb.append(loggerContext).append(" - ");
}
sb.append("Available memory: {} / {}");
progressLogger.debug(sb.toString(),
StringUtils.humanReadableByteCount(Runtime.getRuntime().freeMemory(), true),
StringUtils.humanReadableByteCount(Runtime.getRuntime().totalMemory(), true));
}
}
}
private static class ThreadPoolExecutorAbortFutureCallback implements FutureCallback<Object> {
private final ThreadPoolExecutor executor;
private final String loggerContext;
public ThreadPoolExecutorAbortFutureCallback(ThreadPoolExecutor executor, String loggerContext) {
super();
this.executor = executor;
this.loggerContext = loggerContext;
}
@Override
public void onSuccess(Object result) {
// Do nothing
}
@Override
public void onFailure(Throwable t) {
Collection<Runnable> nonExecutedTasks = abort();
// Avoids multiple useless message due to the other tasks's cancellation when aborting
if (!nonExecutedTasks.isEmpty()) {
LOGGER.error("{} - Aborted execution of {} tasks because the following exception was caught when running a task",
loggerContext, nonExecutedTasks.size(), t);
}
}
private Collection<Runnable> abort() {
// Stop accepting tasks
executor.shutdown();
// Remove all tasks that were not already executed
List<Runnable> nonExecutedTasks = new ArrayList<Runnable>();
executor.getQueue().drainTo(nonExecutedTasks);
// Cancel these tasks (if relevant)
for (Runnable nonExecutedTask : nonExecutedTasks) {
if (nonExecutedTask instanceof Future<?>) {
Future<?> future = (Future<?>) nonExecutedTask;
future.cancel(false);
}
}
return nonExecutedTasks;
}
}
}