/* * ModeShape (http://www.modeshape.org) * * 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 org.modeshape.common.collection.ring; import java.util.HashSet; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.modeshape.common.CommonI18n; import org.modeshape.common.collection.ring.GarbageCollectingConsumer.Collectable; import org.modeshape.common.logging.Logger; import org.modeshape.common.util.CheckArg; /** * A circular or "ring" buffer that allows entries supplied by a producer to be easily, quickly, and independently consumed by * multiple {@link Consumer consumers}. The design of this ring buffer attempts to eliminate or minimize contention between the * different consumers. The ring buffer can be completely lock-free, although by default the consumers of the ring buffer use a * {@link WaitStrategy} that blocks if they have processed all available entries and are waiting for more to be added. <h2> * Concepts</h2> * <p> * Conceptually, this buffer consists of a fixed-sized ring of elements; entries are added at the ring's "cursor" while multiple * consumers follow behind the cursor processing each of the entries as quickly as they can. Each consumer runs in its own thread, * and work toward the cursor at their own pace, independently of all other consumers. Most importantly, every consumer sees the * exact same order of entries. * </p> * <p> * When the ring buffer starts out, it is empty and the cursor is at the starting position. As entries are added, the cursor * travels around the ring, keeping track of its position and the position of all consumers. The cursor can never "lap" any of the * consumers, and this ensures that the consumers see a consistent and ordered set of entries. Typically, consumers are fast * enough that they trail relatively closely behind the cursor; plus, ring buffers are usually sized large enough so that the * cursor rarely (if ever) closes on the slowest consumer. (If this does happen, consider increasing the size of the buffer or * changing the consumers to process the entries more quickly, perhaps using a separate durable queue for those slow consumers.) * </p> * <h2>Consumers</h2> * <p> * Consumers can be added after the ring buffer has entries, but such consumers will only see those entries that are added after * the consumer has been attached to the buffer. Additionally, the ring buffer guarantees that the consumers will be called from a * single thread, so consumers do <em>not</em> need to be concurrent or thread-safe. * </p> * <h2>Batching</h2> * <p> * Even though there is almost no locking within the ring buffer, the ring buffer uses another technique to make it as fast as * possible: batching. A producer can add multiple entries, called a "batch", at once. So rather than having to check for each * entry the the values that are shared among the different threads, adding entries via a batch means the shared data needs to be * checked only once per batch. * </p> * <p> * The consumer threads also process batches, although most of this is hidden within the runnable that calls the * {@link Consumer#consume(Object, long, long)} method. When ready to process an entry, this code asks for one entry and will get * as many entries that are available. All of the returned entries can then be processed without having to check any of the shared * data. * </p> * <h2>Shutdown</h2> * <p> * The {@link #shutdown()} method is a graceful termination that immediately prevents adding new entries and that allows all * consumer threads to continue processing all previously-added entries. When each thread has consumed all entries, the consumer's * thread will terminate and the consumer "unregistered" from the ring buffer. The method will block until all consumers have * completed and are terminated. * </p> * <p> * Once a ring buffer has been shutdown, it cannot be restarted. * </p> * * @param <T> the type of entries stored in the buffer * @param <C> the type of consumer * @author Randall Hauch (rhauch@redhat.com) */ public final class RingBuffer<T, C> { private final int bufferSize; private final int mask; protected final Cursor cursor; private final Object[] buffer; private final Executor executor; protected final AtomicBoolean addEntries = new AtomicBoolean(true); protected final ConsumerAdapter<T, C> consumerAdapter; private final Set<ConsumerRunner> consumers = new CopyOnWriteArraySet<>(); private final GarbageCollectingConsumer gcConsumer; private final Lock producerLock; protected final Logger logger = Logger.getLogger(getClass()); RingBuffer( String name, Cursor cursor, Executor executor, ConsumerAdapter<T, C> consumerAdapter, boolean gcEntries, boolean singleProducer ) { this.cursor = cursor; this.bufferSize = cursor.getBufferSize(); CheckArg.isPositive(bufferSize, "cursor.getBufferSize()"); CheckArg.isPowerOfTwo(bufferSize, "cursor.getBufferSize()"); this.mask = bufferSize - 1; this.buffer = new Object[bufferSize]; this.executor = executor; this.consumerAdapter = consumerAdapter; if (gcEntries) { this.gcConsumer = this.cursor.createGarbageCollectingConsumer(new Collectable() { @Override public void collect( long position ) { // System.out.println("---- CLEAR " + position); clearEntry(position); } }); this.executor.execute(gcConsumer); } else { this.gcConsumer = null; } if (singleProducer) { // There is but one thread calling 'add', so no need for alock. Create an impl that does nothing ... producerLock = new NoOpLock(); } else { // Multiple threads can call 'add', so use a real lock ... producerLock = new ReentrantLock(); } } /** * Add to this buffer a single entry. This method blocks if there is no room in the ring buffer, providing back pressure on * the caller in such cases. Note that if this method blocks for any length of time, that means at least one consumer has yet * to process all of the entries that are currently in the ring buffer. In such cases, consider whether a larger ring buffer * is warranted. * * @param entry the entry to be added; may not be null * @return true if the entry was added, or false if the buffer has been {@link #shutdown()} */ public boolean add( T entry ) { assert entry != null; if (!addEntries.get()) return false; try { producerLock.lock(); long position = cursor.claim(); // blocks; if this fails, we will not have successfully claimed and nothing to do ... int index = (int)(position & mask); buffer[index] = entry; return cursor.publish(position); } finally { producerLock.unlock(); } } /** * Add to this buffer multiple entries. This method blocks until it is added. * * @param entries the entries that are to be added; may not be null * @return true if all of the entries were added, or false if the buffer has been {@link #shutdown()} and none of the entries * were added */ public boolean add( T[] entries ) { assert entries != null; if (entries.length == 0 || !addEntries.get()) return false; try { producerLock.lock(); long position = cursor.claim(entries.length); // blocks for (int i = 0; i != entries.length; ++i) { int index = (int)(position & mask); buffer[index] = entries[i]; } return cursor.publish(position); } finally { producerLock.unlock(); } } @SuppressWarnings( "unchecked" ) protected T getEntry( long position ) { if (position < (cursor.getCurrent() - bufferSize)) { // The cursor has already overwritten the entry ... return null; } int index = (int)(position & mask); return (T)buffer[index]; } protected void clearEntry( long position ) { if (position < (cursor.getCurrent() - bufferSize)) { // The cursor has already overwritten the entry ... return; } int index = (int)(position & mask); buffer[index] = null; } /** * Add the supplied consumer, and have it start processing entries in a separate thread. * <p> * Note that the thread will block when there are no more entries to be consumed. If the thread gets a timeout when waiting * for an entry, this method will retry the wait only one time before stopping. * </p> * <p> * The consumer is automatically removed from the ring buffer when it returns {@code false} from its * {@link Consumer#consume(Object, long, long)} method. * </p> * * @param consumer the component that will process the entries; may not be null * @return true if the consumer was added, or false if the consumer was already registered with this buffer */ public boolean addConsumer( final C consumer ) { return addConsumer(consumer, 1); } /** * Add the supplied consumer, and have it start processing entries in a separate thread. * <p> * The consumer is automatically removed from the ring buffer when it returns {@code false} from its * {@link Consumer#consume(Object, long, long)} method. * </p> * * @param consumer the component that will process the entries; may not be null * @param timesToRetryUponTimeout the number of times that the thread should retry after timing out while waiting for the next * entry; retries will not be attempted if the value is less than 1 * @return true if the consumer was added, or false if the consumer was already registered with this buffer * @throws IllegalStateException if the ring buffer has already been {@link #shutdown()} */ public boolean addConsumer( final C consumer, final int timesToRetryUponTimeout ) { if (!addEntries.get()) { throw new IllegalStateException(); } ConsumerRunner runner = new ConsumerRunner(consumer, timesToRetryUponTimeout); if (gcConsumer != null) gcConsumer.stayBehind(runner.getPointer()); // Try to add the runner instance, with equality based upon consumer instance equality ... if (!consumers.add(runner)) return false; // It was added, so start it ... executor.execute(runner); return true; } /** * Remove the supplied consumer, and block until it stops running and is closed and removed from this buffer. The consumer is * removed at the earliest conevenient point, and will stop seeing entries as soon as it is removed. * * @param consumer the consumer component to be removed entry; retries will not be attempted if the value is less than 1 * @return true if the consumer was removed, stopped, and closed, or false if the supplied consumer was not actually * registered with this buffer (it may have completed) * @throws IllegalStateException if the ring buffer has already been {@link #shutdown()} */ public boolean remove( C consumer ) { if (consumer != null) { // Iterate through the map to find the runner that owns this consumer ... ConsumerRunner match = null; for (ConsumerRunner runner : consumers) { if (runner.getConsumer().equals(consumer)) { match = runner; break; } } // Try to remove the matching runner (if we found one) from our list ... if (match != null) { // Tell the thread to stop and wait for it, after which it will have been removed from our map ... match.close(); return true; } } // We either didn't find it, or we found it but something else remove it while we searched ... return false; } /** * Method called by the {@link ConsumerRunner#run()} method just before the method returns and the thread terminates. This * method invocation allows this buffer to clean up its reference to the runner. * * @param runner the runner that has completed */ protected void disconnect( ConsumerRunner runner ) { this.consumers.remove(runner); if (gcConsumer != null) gcConsumer.ignore(runner.getPointer()); } protected int getBufferSize() { return bufferSize; } /** * Checks if there are any consumers registered. * * @return {@code true} if this buffer has any consumers, {@code false} otherwise. */ public boolean hasConsumers() { return !this.consumers.isEmpty(); } /** * Shutdown this ring buffer by preventing any further entries, but allowing all existing entries to be processed by all * consumers. */ public void shutdown() { // Prevent new entries from being added ... this.addEntries.set(false); // Mark the cursor as being finished; this will stop all consumers from waiting for a batch ... this.cursor.complete(); // Each of the consumer threads will complete the batch they're working on, but will then terminate ... // Stop the garbage collection thread (if running) ... if (this.gcConsumer != null) this.gcConsumer.close(); // Now, block until all the runners have completed ... for (ConsumerRunner runner : new HashSet<>(consumers)) { // use a copy of the runners; they're removed when they close runner.waitForCompletion(); } assert consumers.isEmpty(); } /** * Adapts the {@link #consume(Object, Object, long, long)}, {@link #close(Object)} and * {@link #handleException(Object, Throwable, Object, long, long)} methods to other methods on an unknown type. * * @param <EntryType> the type of event * @param <ConsumerType> the type of consumer * @author Randall Hauch (rhauch@redhat.com) */ public static interface ConsumerAdapter<EntryType, ConsumerType> { /** * Consume an entry from the ring buffer. Generally all exceptions should be handled within this method; any exception * thrown will result in the {@link #handleException(Object, Throwable, Object, long, long)} being called. * * @param consumer the consumer instance that is to consume the event; never null * @param entry the entry; will not be null * @param position the position of the entry within in the ring buffer; this is typically a monotonically-increasing value * @param maxPosition the maximum position of entries in the ring buffer that are being consumed within the same batch; * this will be greater or equal to {@code position} * @return {@code true} if the consumer should continue processing the next entry, or {@code false} if this consumer is to * stop processing any more entries (from this or subsequent batches); returning {@code false} provides a way for * the consumer to signal that it should no longer be used */ boolean consume( ConsumerType consumer, EntryType entry, long position, long maxPosition ); /** * Called by the {@link RingBuffer} when the {@link #consume(Object, Object, long, long)} method returns false, or when * the buffer has been shutdown and the consumer has {@link #consume(Object, Object, long, long) consumed} all entries in * the now-closed buffer. * <p> * This method allows any resources used by the consumer to be cleaned up when no longer needed * </p> * * @param consumer the consumer instance that is being closed; never null */ void close( ConsumerType consumer ); /** * Handle an exception that was thrown from the {@link #consume(Object, Object, long, long)}. * * @param consumer the consumer instance that is to consume the event; never null * @param t the exception; never null * @param entry the entry during the consumption of which generated the exception; will not be null * @param position the position of the entry within in the ring buffer; this is typically a monotonically-increasing value * @param maxPosition the maximum position of entries in the ring buffer that are being consumed within the same batch; * this will be greater or equal to {@code position} */ void handleException( ConsumerType consumer, Throwable t, EntryType entry, long position, long maxPosition ); } protected class ConsumerRunner implements Runnable { private final C consumer; private final PointerBarrier barrier; private final Pointer pointer; private final int timesToRetryUponTimeout; private final AtomicBoolean runThread = new AtomicBoolean(true); private final CountDownLatch stopLatch = new CountDownLatch(1); protected ConsumerRunner( C consumer, final int timesToRetryUponTimeout ) { this.consumer = consumer; this.timesToRetryUponTimeout = timesToRetryUponTimeout; // Create a new barrier and a new pointer for consumer ... this.barrier = cursor.newBarrier(); this.pointer = cursor.newPointer(); // the cursor will not wrap beyond this pointer } protected Pointer getPointer() { return pointer; } protected C getConsumer() { return consumer; } @Override public int hashCode() { return consumer.hashCode(); } @Override public boolean equals( Object obj ) { if (this == obj) return true; if (obj instanceof RingBuffer.ConsumerRunner) { @SuppressWarnings( "unchecked" ) ConsumerRunner that = (ConsumerRunner)obj; return this.consumer.equals(that.consumer); } return false; } public void close() { if (this.runThread.compareAndSet(true, false)) { try { this.barrier.close(); // Need to wake up any dependent consumers/thread (e.g., garbage collection) ... cursor.signalConsumers(); this.stopLatch.await(); } catch (InterruptedException e) { // The thread was interrupted ... Thread.interrupted(); // do nothing ... } } } protected void waitForCompletion() { try { stopLatch.await(); } catch (InterruptedException e) { // The thread was interrupted ... Thread.interrupted(); // do nothing ... } } @Override public void run() { boolean consume = true; try { int retry = timesToRetryUponTimeout; while (consume && runThread.get()) { T entry = null; long next = pointer.get() + 1L; try { // Try to find the next position we can read to ... long maxPosition = barrier.waitFor(next); while (next <= maxPosition) { entry = getEntry(next); try { if (!consumerAdapter.consume(consumer, entry, next, maxPosition)) { // The consumer is done, so break out of the loop and clean up ... consume = false; break; } } catch (Throwable t) { consumerAdapter.handleException(consumer, t, entry, next, maxPosition); } next = pointer.incrementAndGet() + 1L; retry = timesToRetryUponTimeout; } if (maxPosition < 0) { // The buffer has been shutdown and there are no more positions, so we're done ... return; } } catch (TimeoutException e) { // It took too long to wait, but keep trying ... --retry; if (retry < 0) { return; } } catch (InterruptedException e) { // The thread was interrupted ... Thread.interrupted(); break; } catch (RuntimeException e) { // Don't retry this entry, so just advance the pointer and continue ... pointer.incrementAndGet(); } } } finally { // We are done ... try { consume = false; // Tell the cursor to ignore our pointer ... cursor.ignore(pointer); } finally { try { consumerAdapter.close(consumer); } catch (Throwable t) { logger.error(t, CommonI18n.errorWhileClosingRingBufferConsumer, consumer, t.getMessage()); } finally { try { disconnect(this); } finally { stopLatch.countDown(); } } } } } } protected static final class NoOpLock implements Lock { @Override public void lock() { } @Override public void unlock() { } @Override public void lockInterruptibly() { } @Override public boolean tryLock() { return false; } @Override public boolean tryLock( long time, TimeUnit unit ) { return false; } @Override public Condition newCondition() { return null; } } }