/*
* Copyright (c) 2008-2017, Hazelcast, Inc. All Rights Reserved.
*
* 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.hazelcast.internal.util.concurrent;
import com.hazelcast.util.concurrent.BackoffIdleStrategy;
import com.hazelcast.util.concurrent.IdleStrategy;
import com.hazelcast.util.function.Predicate;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.Collection;
import java.util.Queue;
import static java.lang.Thread.currentThread;
import static java.util.concurrent.TimeUnit.MICROSECONDS;
import static java.util.concurrent.locks.LockSupport.unpark;
/**
* A many-to-one conveyor of interthread messages. Allows a setup where
* communication from N submitter threads to 1 drainer thread happens over N
* one-to-one concurrent queues. Queues are numbered from 0 to N-1 and the queue
* at index 0 is the <i>default</i> queue. There are some convenience methods
* which assume the usage of the default queue.
* <p/>
* Allows the drainer thread to signal completion and failure to the submitters
* and make their blocking {@code submit()} calls fail with an exception. This
* mechanism supports building an implementation which is both starvation-safe
* and uses bounded queues with blocking queue submission.
* <p/>
* There is a further option for the drainer to apply immediate backpressure to
* the submitter by invoking {@link #backpressureOn()}. This will make the
* {@code submit()} invocations block after having successfully submitted their
* item, until the drainer calls {@link #backpressureOff()} or fails. This
* mechanism allows the drainer to apply backpressure and keep draining the queue,
* thus letting all submitters progress until after submitting their item. Such an
* arrangement eliminates a class of deadlock patterns where the submitter blocks
* to submit the item that would have made the drainer remove backpressure.
* <p/>
* Does not manage drainer threads. There should be only one drainer thread at a
* time.
* <p/>
* <h3>Usage example</h3>
* <pre>{@code
* // 1. Set up the concurrent conveyor
*
* final int queueCapacity = 128;
* final Runnable doneItem = new Runnable() { public void run() {} };
* final QueuedPipe<Runnable>[] qs = new QueuedPipe[2];
* qs[0] = new OneToOneConcurrentArrayQueue<Runnable>(queueCapacity);
* qs[1] = new OneToOneConcurrentArrayQueue<Runnable>(queueCapacity);
* final ConcurrentConveyor<Runnable> conveyor = concurrentConveyor(doneItem, qs);
*
* // 2. Set up the drainer thread
*
* final Thread drainer = new Thread(new Runnable() {
* private int submitterGoneCount;
*
* @Override
* public void run() {
* conveyor.drainerArrived();
* try {
* final List<Runnable> batch = new ArrayList<Runnable>(queueCapacity);
* while (submitterGoneCount < conveyor.queueCount()) {
* for (int i = 0; i < conveyor.queueCount(); i++) {
* batch.clear();
* conveyor.drainTo(i, batch);
* // process(batch) should increment submitterGoneCount
* // each time it encounters the conveyor.submitterGoneItem()
* process(batch);
* }
* }
* conveyor.drainerDone();
* } catch (Throwable t) {
* conveyor.drainerFailed(t);
* }
* }
* });
* drainer.start();
*
* // 3. Set up the submitter threads
*
* for (final int submitterIndex : new int[] { 0, 1, 2, 3 }) {
* new Thread(new Runnable() {
* @Override
* public void run() {
* final QueuedPipe<Runnable> q = conveyor.queue(submitterIndex);
* try {
* while (!askedToStop) {
* conveyor.submit(q, new Item());
* }
* } finally {
* try {
* conveyor.submit(q, conveyor.submitterGoneItem());
* } catch (ConcurrentConveyorException e) {
* // logger.warning() || rethrow || ...
* }
* }
* }
* }).start();
* }
* }</pre>
*/
@SuppressWarnings("checkstyle:interfaceistype")
public class ConcurrentConveyor<E> {
/**
* How many times to busy-spin while waiting to submit to the work queue.
*/
public static final int SUBMIT_SPIN_COUNT = 1000;
/**
* How many times to yield while waiting to submit to the work queue.
*/
public static final int SUBMIT_YIELD_COUNT = 200;
/**
* Max park microseconds while waiting to submit to the work queue.
*/
public static final long SUBMIT_MAX_PARK_MICROS = 200;
/**
* Idling strategy used by the {@code submit()} methods.
*/
public static final IdleStrategy SUBMIT_IDLER = new BackoffIdleStrategy(
SUBMIT_SPIN_COUNT, SUBMIT_YIELD_COUNT, 1, MICROSECONDS.toNanos(SUBMIT_MAX_PARK_MICROS));
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
private static final Throwable REGULAR_DEPARTURE = regularDeparture();
private final QueuedPipe<E>[] queues;
private final E submitterGoneItem;
private volatile boolean backpressure;
private volatile Thread drainer;
private volatile Throwable drainerDepartureCause;
private volatile int liveQueueCount;
ConcurrentConveyor(E submitterGoneItem, QueuedPipe<E>... queues) {
if (queues.length == 0) {
throw new IllegalArgumentException("No concurrent queues supplied");
}
this.submitterGoneItem = submitterGoneItem;
this.queues = validateAndCopy(queues);
this.liveQueueCount = queues.length;
}
private QueuedPipe<E>[] validateAndCopy(QueuedPipe<E>[] queues) {
final QueuedPipe<E>[] safeCopy = new QueuedPipe[queues.length];
for (int i = 0; i < queues.length; i++) {
if (queues[i] == null) {
throw new IllegalArgumentException("Queue at index " + i + " is null");
}
safeCopy[i] = queues[i];
}
return safeCopy;
}
/**
* Creates a new concurrent conveyor.
*
* @param submitterGoneItem the object that a submitter thread can use to
* signal it's done submitting
* @param queues the concurrent queues the conveyor will manage
*/
public static <E1> ConcurrentConveyor<E1> concurrentConveyor(
E1 submitterGoneItem, QueuedPipe<E1>... queues
) {
return new ConcurrentConveyor<E1>(submitterGoneItem, queues);
}
/**
* @return the last item that the submitter thread submits to the conveyor
*/
public final E submitterGoneItem() {
return submitterGoneItem;
}
/**
* Returns the size of the array holding the concurrent queues. Initially
* (when the conveyor is constructed) each array slot should point to a
* distinct concurrent queue; therefore the size of the array matches the
* number of queues. Some array slots may be nulled out later by calling
* {@link #removeQueue(int)}, but this method will keep reporting the same
* number. The intended use case for this method is giving the upper bound
* for a loop that iterates over all queues. Since queue indices never
* change, this number must stay the same.
*/
public final int queueCount() {
return queues.length;
}
/**
* Returns the number of remaining live queues, i.e., {@link #queueCount()}
* minus the number of queues nulled out by calling {@link #removeQueue(int)}.
*/
public final int liveQueueCount() {
return liveQueueCount;
}
/**
* @return the concurrent queue at the given index
*/
public final QueuedPipe<E> queue(int index) {
return queues[index];
}
@SuppressFBWarnings(value = "VO_VOLATILE_INCREMENT",
justification = "liveQueueCount is updated only by the drainer thread")
public final boolean removeQueue(int index) {
final boolean didRemove = queues[index] != null;
queues[index] = null;
liveQueueCount--;
return didRemove;
}
/**
* Offers an item to the queue at the given index.
*
* @return whether the item was accepted by the queue
*/
public final boolean offer(int queueIndex, E item) {
return offer(queues[queueIndex], item);
}
/**
* Offers an item to the given queue. No check is performed that the queue
* actually belongs to this conveyor.
*
* @return whether the item was accepted by the queue
* @throws ConcurrentConveyorException if the draining thread has already left
*/
public final boolean offer(Queue<E> queue, E item) throws ConcurrentConveyorException {
if (queue.offer(item)) {
return true;
} else {
checkDrainerGone();
unparkDrainer();
return false;
}
}
/**
* Blocks until successfully inserting the given item to the given queue.
* No check is performed that the queue actually belongs to this conveyor.
* If the {@code #backpressure} flag is raised on this conveyor at the time
* the item has been submitted, further blocks until the flag is lowered.
*
* @throws ConcurrentConveyorException if the current thread is interrupted
* or the draining thread has already left
*/
public final void submit(Queue<E> queue, E item) throws ConcurrentConveyorException {
for (long idleCount = 0; !queue.offer(item); idleCount++) {
SUBMIT_IDLER.idle(idleCount);
checkDrainerGone();
unparkDrainer();
checkInterrupted();
}
for (long idleCount = 0; backpressure; idleCount++) {
SUBMIT_IDLER.idle(idleCount);
checkInterrupted();
}
}
/**
* Drains a batch of items from the default queue into the supplied collection.
*
* @return the number of items drained
*/
public final int drainTo(Collection<? super E> drain) {
return drain(queues[0], drain, Integer.MAX_VALUE);
}
/**
* Drains a batch of items from the queue at the supplied index into the
* supplied collection.
*
* @return the number of items drained
*/
public final int drainTo(int queueIndex, Collection<? super E> drain) {
return drain(queues[queueIndex], drain, Integer.MAX_VALUE);
}
/**
* Drains a batch of items from the queue at the supplied index to the
* supplied {@code itemHandler}. Stops draining, after the {@code itemHandler}
* returns false;
*
* @return the number of items drained
*/
public final int drain(int queueIndex, Predicate<? super E> itemHandler) {
return queues[queueIndex].drain(itemHandler);
}
/**
* Drains no more than {@code limit} items from the default queue into the
* supplied collection.
*
* @return the number of items drained
*/
public final int drainTo(Collection<? super E> drain, int limit) {
return drain(queues[0], drain, limit);
}
/**
* Drains no more than {@code limit} items from the queue at the supplied
* index into the supplied collection.
*
* @return the number of items drained
*/
public final int drainTo(int queueIndex, Collection<? super E> drain, int limit) {
return drain(queues[queueIndex], drain, limit);
}
/**
* Called by the drainer thread to signal that it has started draining the
* queue.
*/
public final void drainerArrived() {
drainerDepartureCause = null;
drainer = currentThread();
}
/**
* Called by the drainer thread to signal that it has failed and will drain
* no more items from the queue.
*
* @param t the drainer's failure
*/
public final void drainerFailed(Throwable t) {
if (t == null) {
throw new NullPointerException("ConcurrentConveyor.drainerFailed(null)");
}
drainer = null;
drainerDepartureCause = t;
}
/**
* Called by the drainer thread to signal that it is done draining the queue.
*/
public final void drainerDone() {
drainer = null;
drainerDepartureCause = REGULAR_DEPARTURE;
}
/**
* @return whether the drainer thread has left
*/
public final boolean isDrainerGone() {
return drainerDepartureCause != null;
}
/**
* Checks whether the drainer thread has left and throws an exception if it
* has. If the drainer thread has failed, its failure will be the cause of
* the exception thrown.
*/
public final void checkDrainerGone() {
final Throwable cause = drainerDepartureCause;
if (cause == REGULAR_DEPARTURE) {
throw new ConcurrentConveyorException("Queue drainer has already left");
}
propagateDrainerFailure(cause);
}
/**
* Blocks until the drainer thread leaves.
*/
public final void awaitDrainerGone() {
for (long i = 0; !isDrainerGone(); i++) {
SUBMIT_IDLER.idle(i);
}
propagateDrainerFailure(drainerDepartureCause);
}
/**
* Raises the {@code backpressure} flag, which will make the caller of
* {@code submit} to block until the flag is lowered.
*/
public final void backpressureOn() {
backpressure = true;
}
/**
* Lowers the {@code backpressure} flag.
*/
public final void backpressureOff() {
backpressure = false;
}
private int drain(QueuedPipe<E> q, Collection<? super E> drain, int limit) {
return q.drainTo(drain, limit);
}
private void unparkDrainer() {
final Thread drainer = this.drainer;
if (drainer != null) {
unpark(drainer);
}
}
private static void propagateDrainerFailure(Throwable cause) {
if (cause != null && cause != REGULAR_DEPARTURE) {
throw new ConcurrentConveyorException("Queue drainer failed", cause);
}
}
private static void checkInterrupted() throws ConcurrentConveyorException {
if (currentThread().isInterrupted()) {
throw new ConcurrentConveyorException("Thread interrupted");
}
}
private static ConcurrentConveyorException regularDeparture() {
final ConcurrentConveyorException e = new ConcurrentConveyorException("Regular departure");
e.setStackTrace(new StackTraceElement[0]);
return e;
}
}