/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.toolkit.modules.concurrency.internal; import java.util.Deque; import java.util.LinkedList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import de.rcenvironment.toolkit.modules.concurrency.api.AsyncCallbackExceptionPolicy; import de.rcenvironment.toolkit.modules.concurrency.api.AsyncOrderedExecutionQueue; import de.rcenvironment.toolkit.modules.concurrency.api.AsyncTaskService; import de.rcenvironment.toolkit.modules.concurrency.api.TaskDescription; import de.rcenvironment.toolkit.modules.statistics.api.CounterCategory; import de.rcenvironment.toolkit.modules.statistics.api.StatisticsFilterLevel; /** * Default {@link AsyncOrderedExecutionQueue} implementation. * * @author Robert Mischke */ public class AsyncOrderedExecutionQueueImpl implements AsyncOrderedExecutionQueue { private static final String ASYNC_TASK_DESCRIPTION = AsyncOrderedExecutionQueue.STATS_COUNTER_SHARED_CATEGORY_NAME; private static final int MAXIMUM_QUEUE_CANCEL_WAIT_SECONDS = 30; private final CountDownLatch cancelCompleteLatch = new CountDownLatch(1); /** * A {@link Runnable} that dispatches the first element from the queue, and enqueues itself again if the queue is not empty afterwards. * * @author Robert Mischke */ private final class DispatchRunnable implements Runnable { @Override @TaskDescription(ASYNC_TASK_DESCRIPTION) public void run() { // dispatch all currently queued elements in the same execution to avoid thread switching and thread pool overhead while (dispatchSingleElement()) { // sometimes, CheckStyle rules just don't make sense... @SuppressWarnings("unused") int i = 0; // (this should be eliminated by the compiler) } } /** * @return true if dispatching should continue, ie the queue is neither empty nor canceled */ private boolean dispatchSingleElement() { final Runnable preExecutionFirst; synchronized (queue) { preExecutionFirst = queue.peekFirst(); } // cancel marker reached? ("poison pill" approach) if (preExecutionFirst == null) { log.debug("Queue cancelled, discarding queued trigger; queue id: " + getLogId()); cancelCompleteLatch.countDown(); // note: leaving marker in queue for other queued dispatchers return false; // do not continue dispatching } try { if (elementCounter.isEnabled()) { elementCounter.countClass(preExecutionFirst); } preExecutionFirst.run(); } catch (RuntimeException e) { switch (exceptionPolicy) { case LOG_AND_CANCEL_LISTENER: log.error("Error in asynchronous callback; shutting down queue (as defined by exception policy); queue id: " + getLogId(), e); AsyncOrderedExecutionQueueImpl.this.cancelAsync(); return false; // do not continue dispatching case LOG_AND_PROCEED: log.error("Error in asynchronous callback; continuing (as defined by exception policy); queue id: " + getLogId(), e); break; default: throw new IllegalStateException(); } } synchronized (queue) { // do not remove the first element in case it is the poison pill to ensure proper cancellation Runnable postExecutionFirst = queue.peekFirst(); if (postExecutionFirst == null) { log.debug("Queue cancelled during a task's execution; stopping dispatcher " + "and waiting for the running task to complete; queue id: " + getLogId()); cancelCompleteLatch.countDown(); return false; // do not continue dispatching } postExecutionFirst = queue.removeFirst(); if (preExecutionFirst != postExecutionFirst) { throw new IllegalStateException("Queue corruption (queue id: " + getLogId() + ")"); } // if this was not the last queued callback, continue dispatching return !queue.isEmpty(); } } } private final AsyncCallbackExceptionPolicy exceptionPolicy; private final AsyncTaskService threadPool; private final Deque<Runnable> queue; private final Runnable dispatchRunnable; private final CounterCategory elementCounter; private final Log log = LogFactory.getLog(getClass()); public AsyncOrderedExecutionQueueImpl(final AsyncCallbackExceptionPolicy exceptionPolicy, final ConcurrencyUtilsServiceHolder internalServiceHolder) { this.exceptionPolicy = exceptionPolicy; this.threadPool = internalServiceHolder.getAsyncTaskService(); this.queue = new LinkedList<Runnable>(); this.dispatchRunnable = new DispatchRunnable(); this.elementCounter = internalServiceHolder.getStatisticsTrackerService().getCounterCategory( "AsyncOrderedExecutionQueue elements dispatched", StatisticsFilterLevel.DEVELOPMENT); } /** * Enqueues a {@link Runnable} task for execution. All tasks are guaranteed to be executed in the order they were enqueued in. * * @param task the task to enqueue */ @Override public void enqueue(final Runnable task) { boolean isFirst; synchronized (queue) { queue.addLast(task); // if this is the first queued callback, enqueue a new dispatch task isFirst = queue.size() == 1; } if (isFirst) { threadPool.execute(dispatchRunnable); } } /** * Gracefully cancels this queue. All pending elements are removed, and no more tasks are started. This call does not interrupt the * currently running task, if one exists, but it does not wait for its completion either. */ @Override public void cancelAsync() { synchronized (queue) { if (queue.isEmpty()) { cancelCompleteLatch.countDown(); } queue.clear(); queue.add(null); // add cancel marker } } /** * Gracefully cancels this queue. All pending elements are removed, and no more tasks are started. If a task is currently running, it is * not interrupted, but this method will wait until it completes by itself (with a timeout). * * @throws TimeoutException if an internal time limit (currently 30 seconds) is exceeded while waiting for the current task to complete */ @Override public void cancelAndWaitForLastRunningTask() throws TimeoutException { cancelAsync(); try { if (!cancelCompleteLatch.await(MAXIMUM_QUEUE_CANCEL_WAIT_SECONDS, TimeUnit.SECONDS)) { throw new TimeoutException("Maximum wait time for queue shutdown exceeded"); } } catch (InterruptedException e) { log.warn("Thread interrupted while waiting for queue shutdown; queue id: " + getLogId()); } } /** * Gracefully cancels this queue. All pending elements are removed, and no more tasks are started. This call does not interrupt the * currently running task, if one exists. * * @param waitForShutdown if true, this task waits until the last running job (if it exists) has finished; otherwise, this method always * returns immediately * @throws TimeoutException if an internal time limit (currently 30 seconds) is exceeded while waiting */ @Override @Deprecated // use the above methods for clarity public void cancel(boolean waitForShutdown) throws TimeoutException { if (waitForShutdown) { cancelAndWaitForLastRunningTask(); } else { cancelAsync(); } } private int getLogId() { return System.identityHashCode(this); } }