/** * Copyright 2010 Molindo GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package at.molindo.esi4j.chain.impl; import java.io.Serializable; import java.util.LinkedHashMap; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.RejectedExecutionHandler; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.elasticsearch.action.ListenableActionFuture; import org.elasticsearch.action.bulk.BulkItemResponse; import org.elasticsearch.action.bulk.BulkRequestBuilder; import org.elasticsearch.action.bulk.BulkResponse; import org.elasticsearch.client.Client; import at.molindo.esi4j.action.BulkResponseWrapper; import at.molindo.esi4j.chain.Esi4JBatchedEntityResolver; import at.molindo.esi4j.chain.Esi4JEntityResolver; import at.molindo.esi4j.chain.Esi4JEntityTask; import at.molindo.esi4j.core.Esi4JOperation; import at.molindo.esi4j.mapping.ObjectKey; import at.molindo.utils.collections.ArrayUtils; import at.molindo.utils.collections.ListMap; /** * wrapping a {@link ThreadPoolExecutor} to execute {@link Esi4JEntityTask}s asynchronously. This implementations * provides a best-effort ordering of executed tasks */ public class QueuedTaskExecutor { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(QueuedTaskExecutor.class); private static final AtomicInteger EXECUTOR_NUMBER = new AtomicInteger(1); private final int _executorNumber = EXECUTOR_NUMBER.getAndIncrement(); private final AtomicInteger _threadNumber = new AtomicInteger(1); private final QueuedTaskProcessor _queuedTaskProcessor; private final Esi4JBatchedEntityResolver _entityResolver; private final ThreadPoolExecutor _executorService; /* * TODO used to block execution of Esi4JEntityTasks if a SerializableEsi4JOperation is submitted. However, it's not * the time of submission but the start of execution that determines order */ private final ReentrantReadWriteLock _executionOrderLock = new ReentrantReadWriteLock(true); private final int _poolSize; public QueuedTaskExecutor(final QueuedTaskProcessor queuedTaskProcessor, final Esi4JBatchedEntityResolver entityResolver) { if (queuedTaskProcessor == null) { throw new NullPointerException("queuedTaskProcessor"); } _queuedTaskProcessor = queuedTaskProcessor; _entityResolver = entityResolver; // TODO make configurable _poolSize = (Runtime.getRuntime().availableProcessors() + 1) / 2; _executorService = newExecutorService(); } private ThreadPoolExecutor newExecutorService() { log.info("creating new QueuedTaskExecutor with " + _poolSize + " threads"); final ThreadFactory factory = new ThreadFactory() { @Override public Thread newThread(final Runnable r) { return new ExecutorThread(r); } }; final RejectedExecutionHandler handler = new RejectedExecutionHandler() { @Override public void rejectedExecution(final Runnable r, final ThreadPoolExecutor executor) { log.warn("executor rejected execution of bulk index task"); } }; return new ThreadPoolExecutor(_poolSize, _poolSize, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), factory, handler); } public void execute(final Esi4JEntityTask[] tasks) { if (!ArrayUtils.empty(tasks)) { if (_entityResolver != null) { final ListMap<ObjectKey, Integer> taskIndices = replaceEntities(tasks); resolveDuplicates(tasks, taskIndices); } _executorService.execute(new BulkIndexRunnable(tasks)); } } /** * call {@link Esi4JEntityResolver#replaceEntity(Object)} for each task. At the same time, we create a map of * {@link ObjectKey}s. */ private ListMap<ObjectKey, Integer> replaceEntities(final Esi4JEntityTask[] tasks) { final ListMap<ObjectKey, Integer> map = new ObjectKeyListMap(tasks.length); for (int i = 0; i < tasks.length; i++) { final Esi4JEntityTask task = tasks[i]; if (task != null) { task.replaceEntity(_entityResolver); map.add(task.toObjectKey(_entityResolver), i); } } return map; } /** * reduce number of operations by replacing duplicates. If a task isn't an update, we ignore everything before it. */ static void resolveDuplicates(final Esi4JEntityTask[] tasks, final ListMap<ObjectKey, Integer> map) { for (final Map.Entry<ObjectKey, List<Integer>> e : map.entrySet()) { final List<Integer> taskIndices = e.getValue(); if (taskIndices.size() > 1) { // resolving duplicates final ListIterator<Integer> iter = taskIndices.listIterator(taskIndices.size()); boolean overwritePrevious = false; while (iter.hasPrevious()) { final int taskIndex = iter.previous(); if (overwritePrevious) { tasks[taskIndex] = null; iter.remove(); } else if (!tasks[taskIndex].isUpdate()) { overwritePrevious = true; } } } } } public <T> T submit(final SerializableEsi4JOperation<T> operation) { try { final T value = _executorService.submit(new OperationCallable<T>(operation)).get(); if (log.isDebugEnabled()) { log.debug("finished submitted operation"); } return value; } catch (final InterruptedException e) { // TODO handle throw new RuntimeException(e); } catch (final ExecutionException e) { // TODO handle throw new RuntimeException(e); } } public void close() { _executorService.shutdown(); try { _executorService.awaitTermination(60, TimeUnit.SECONDS); } catch (final InterruptedException e) { log.warn("waiting for termination of executor service interrupted", e); } } public QueuedTaskProcessor getTaskProcessor() { return _queuedTaskProcessor; } public Esi4JBatchedEntityResolver getEntityResolver() { return _entityResolver; } /** * thread that references this {@link QueuedTaskExecutor} */ private final class ExecutorThread extends Thread { public ExecutorThread(final Runnable r) { super(r, QueuedTaskProcessor.class.getSimpleName() + "-" + _executorNumber + "-" + _threadNumber.getAndIncrement()); setDaemon(true); } public QueuedTaskExecutor getQueuedTaskExecutor() { return QueuedTaskExecutor.this; } } private static final class OperationCallable<T> implements Callable<T>, Serializable { private static final long serialVersionUID = 1L; // discard during serialization private final SerializableEsi4JOperation<T> _operation; private OperationCallable(final SerializableEsi4JOperation<T> operation) { if (operation == null) { throw new NullPointerException("operation"); } _operation = operation; } @Override public T call() throws Exception { if (_operation == null) { return null; } final QueuedTaskExecutor executor = ((ExecutorThread) Thread.currentThread()).getQueuedTaskExecutor(); // wait for current tasks to complete executor._executionOrderLock.writeLock().lock(); try { return executor.getTaskProcessor().getIndex().execute(_operation); } finally { executor._executionOrderLock.writeLock().unlock(); } } } /** * execute a bulk of {@link Esi4JEntityTask}s */ private static final class BulkIndexRunnable implements Runnable, Serializable { private static final long serialVersionUID = 1L; private final Esi4JEntityTask[] _tasks; /** * @param tasks * might contain <code>null</code> */ public BulkIndexRunnable(final Esi4JEntityTask[] tasks) { _tasks = tasks; } @Override public void run() { final QueuedTaskExecutor executor = ((ExecutorThread) Thread.currentThread()).getQueuedTaskExecutor(); executor._executionOrderLock.readLock().lock(); try { // ensure unlock() executor.getTaskProcessor().onBeforeBulkIndex(); try { // ensure onAfterBulkIndex() index(executor); } finally { executor.getTaskProcessor().onAfterBulkIndex(); } } finally { executor._executionOrderLock.readLock().unlock(); } } private void index(final QueuedTaskExecutor executor) { final Esi4JBatchedEntityResolver entityResolver = executor.getEntityResolver(); if (entityResolver != null) { entityResolver.resolveEntities(_tasks); } final BulkResponseWrapper response = executor.getTaskProcessor().getIndex() .executeBulk(new Esi4JOperation<ListenableActionFuture<BulkResponse>>() { @Override public ListenableActionFuture<BulkResponse> execute(final Client client, final String indexName, final OperationContext helper) { final BulkRequestBuilder bulk = client.prepareBulk(); for (final Esi4JEntityTask _task : _tasks) { if (_task != null) { _task.addToBulk(client, bulk, indexName, helper); } } final ListenableActionFuture<BulkResponse> response = bulk.execute(); return response; } }).actionGet(); int failed = 0; for (final BulkItemResponse item : response.getBulkResponse()) { if (item.isFailed()) { failed++; } } if (failed > 0) { log.warn("failed to index " + failed + " items. index might be out of sync"); } if (log.isDebugEnabled()) { final int indexed = response.getBulkResponse().getItems().length - failed; log.debug("finished bulk indexing " + indexed + " items"); } } } static final class ObjectKeyListMap extends ListMap<ObjectKey, Integer> { private final int _capacity; public ObjectKeyListMap(final int capacity) { _capacity = capacity; } @Override protected Map<ObjectKey, List<Integer>> newMap() { return new LinkedHashMap<>(_capacity * 2, 0.75f, false); } } }