/*
* Copyright 2012 LinkedIn, Inc
*
* 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 com.linkedin.parseq.internal;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
/**
* An executor that provides the following guarantees:
* <p>
* 1. Only one Runnable may be executed at a time
* 2. The completion of a Runnable happens-before the execution of the next Runnable
* <p>
* For more on the happens-before constraint see the {@code java.util.concurrent}
* package documentation.
* <p>
* It is possible for the underlying executor to throw an exception signaling
* that it is not able to accept new work. For example, this can occur with an
* executor that has a bounded queue size and an
* {@link java.util.concurrent.ThreadPoolExecutor.AbortPolicy}. If this occurs
* the executor will run the {@code rejectionHandler} to signal this failure
* to a layer that can more appropriate handle this event.
*
* @author Chris Pettitt (cpettitt@linkedin.com)
* @author Jaroslaw Odzga (jodzga@linkedin.com)
*/
public class SerialExecutor {
/*
* Below is a proof and description of a mechanism that ensures that
* "completion of a Runnable happens-before the execution of the next Runnable".
* Let's call the above proposition P from now on.
*
* A Runnable can only be executed by tryExecuteLoop. tryExecuteLoop makes sure that
* there is a happens-before relationship between the current thread that executes
* tryExecuteLoop and the execution of the next Runnable. Lets call this property HB.
*
* tryExecuteLoop can be invoked in two ways:
*
* 1) Recursively from within the ExecutorLoop.run method after completion of a Runnable
* on a thread belonging to the underlying executor.
* In this case HB ensures that P is true.
*
* 2) Through the SerialExecutor.execute method on an arbitrary thread.
* There are two cases:
* a) The submitted Runnable is the first Runnable executed by this SerialExecutor. In this case P is trivially true.
* b) The submitted Runnable is not the first Runnable executed by this SerialExecutor. In this case the thread that executed
* the last runnable, after it's completion, must have invoked _pendingCount.decrementAndGet, and got 0. Since the
* thread executing SerialExecutor.execute invoked _pendingCount.getAndIncrement, and got the value 0, it means
* that there is a happens-before relationship between the thread completing last Runnable and the current thread
* executing SerialExecutor.execute. Combined with HB1 this means that P is true.
*/
private final Executor _executor;
private final UncaughtExceptionHandler _uncaughtExecutionHandler;
private final Runnable _executorLoop;
private final TaskQueue<PrioritizableRunnable> _queue;
private final AtomicInteger _pendingCount = new AtomicInteger();
private final DeactivationListener _deactivationListener;
public SerialExecutor(final Executor executor,
final UncaughtExceptionHandler uncaughtExecutionHandler,
final DeactivationListener deactivationListener,
final TaskQueue<PrioritizableRunnable> taskQueue,
final boolean drainSerialExecutorQueue) {
ArgumentUtil.requireNotNull(executor, "executor");
ArgumentUtil.requireNotNull(uncaughtExecutionHandler, "uncaughtExecutionHandler" );
ArgumentUtil.requireNotNull(deactivationListener, "deactivationListener" );
_executor = executor;
_uncaughtExecutionHandler = uncaughtExecutionHandler;
_queue = taskQueue;
_deactivationListener = deactivationListener;
_executorLoop = drainSerialExecutorQueue ? new DrainingExecutorLoop() : new NonDrainingExecutorLoop();
}
public void execute(final PrioritizableRunnable runnable) {
_queue.add(runnable);
// Guarantees that execution loop is scheduled only once to the underlying executor.
// Also makes sure that all memory effects of last Runnable are visible to the next Runnable
// in case value returned by decrementAndGet == 0.
if (_pendingCount.getAndIncrement() == 0) {
tryExecuteLoop();
}
}
/*
* This method acts as a happen-before relation between current thread and next Runnable that will
* be executed by this executor because of properties of underlying _executor.execute().
*/
private void tryExecuteLoop() {
try {
_executor.execute(_executorLoop);
} catch (Throwable t) {
_uncaughtExecutionHandler.uncaughtException(t);
}
}
private class DrainingExecutorLoop implements Runnable {
@Override
public void run() {
// Entering state:
// - _queue.size() > 0
// - _pendingCount.get() > 0
for (;;) {
final Runnable runnable = _queue.poll();
try {
runnable.run();
// Deactivation listener is called before _pendingCount.decrementAndGet() so that
// it does not run concurrently with any other Runnable submitted to this Executor.
// _pendingCount.get() == 1 means that there are no more Runnables submitted to this
// executor waiting to be executed. Since _pendingCount can be changed in other threads
// in is possible to get _pendingCount.get() == 1 and _pendingCount.decrementAndGet() > 0
// to be true few lines below.
if (_pendingCount.get() == 1) {
_deactivationListener.deactivated();
}
} catch (Throwable t) {
_uncaughtExecutionHandler.uncaughtException(t);
} finally {
// Guarantees that execution loop is scheduled only once to the underlying executor.
// Also makes sure that all memory effects of last Runnable are visible to the next Runnable
// in case value returned by decrementAndGet == 0.
if (_pendingCount.decrementAndGet() == 0) {
break;
}
}
}
}
}
private class NonDrainingExecutorLoop implements Runnable {
@Override
public void run() {
// Entering state:
// - _queue.size() > 0
// - _pendingCount.get() > 0
final Runnable runnable = _queue.poll();
try {
runnable.run();
// Deactivation listener is called before _pendingCount.decrementAndGet() so that
// it does not run concurrently with any other Runnable submitted to this Executor.
// _pendingCount.get() == 1 means that there are no more Runnables submitted to this
// executor waiting to be executed. Since _pendingCount can be changed in other threads
// in is possible to get _pendingCount.get() == 1 and _pendingCount.decrementAndGet() > 0
// to be true few lines below.
if (_pendingCount.get() == 1) {
_deactivationListener.deactivated();
}
} catch (Throwable t) {
_uncaughtExecutionHandler.uncaughtException(t);
} finally {
// Guarantees that execution loop is scheduled only once to the underlying executor.
// Also makes sure that all memory effects of last Runnable are visible to the next Runnable
// in case value returned by decrementAndGet == 0.
if (_pendingCount.decrementAndGet() > 0) {
// Aside from it's obvious intent it also makes sure that all memory effects are visible
// to the next Runnable
tryExecuteLoop();
}
}
}
}
/**
* A priority queue which stores runnables to be executed within a {@link SerialExecutor}.
* The implementation has to make sure runnables are sorted in the descending order based
* on their priority.
*/
public interface TaskQueue<T extends Prioritizable> {
void add(T value);
T poll();
}
/*
* Deactivation listener is notified when this executor finished executing a Runnable
* and there are no other Runnables waiting in queue.
* It is executed sequentially with respect to other Runnables executed by this Executor.
*/
interface DeactivationListener {
void deactivated();
}
}