001/*
002 * ModeShape (http://www.modeshape.org)
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *       http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.modeshape.common.collection.ring;
018
019import java.util.HashSet;
020import java.util.Set;
021import java.util.concurrent.CopyOnWriteArraySet;
022import java.util.concurrent.CountDownLatch;
023import java.util.concurrent.Executor;
024import java.util.concurrent.TimeUnit;
025import java.util.concurrent.TimeoutException;
026import java.util.concurrent.atomic.AtomicBoolean;
027import java.util.concurrent.locks.Condition;
028import java.util.concurrent.locks.Lock;
029import java.util.concurrent.locks.ReentrantLock;
030import org.modeshape.common.CommonI18n;
031import org.modeshape.common.collection.ring.GarbageCollectingConsumer.Collectable;
032import org.modeshape.common.logging.Logger;
033import org.modeshape.common.util.CheckArg;
034
035/**
036 * A circular or "ring" buffer that allows entries supplied by a producer to be easily, quickly, and independently consumed by
037 * multiple {@link Consumer consumers}. The design of this ring buffer attempts to eliminate or minimize contention between the
038 * different consumers. The ring buffer can be completely lock-free, although by default the consumers of the ring buffer use a
039 * {@link WaitStrategy} that blocks if they have processed all available entries and are waiting for more to be added. <h2>
040 * Concepts</h2>
041 * <p>
042 * Conceptually, this buffer consists of a fixed-sized ring of elements; entries are added at the ring's "cursor" while multiple
043 * consumers follow behind the cursor processing each of the entries as quickly as they can. Each consumer runs in its own thread,
044 * and work toward the cursor at their own pace, independently of all other consumers. Most importantly, every consumer sees the
045 * exact same order of entries.
046 * </p>
047 * <p>
048 * When the ring buffer starts out, it is empty and the cursor is at the starting position. As entries are added, the cursor
049 * travels around the ring, keeping track of its position and the position of all consumers. The cursor can never "lap" any of the
050 * consumers, and this ensures that the consumers see a consistent and ordered set of entries. Typically, consumers are fast
051 * enough that they trail relatively closely behind the cursor; plus, ring buffers are usually sized large enough so that the
052 * cursor rarely (if ever) closes on the slowest consumer. (If this does happen, consider increasing the size of the buffer or
053 * changing the consumers to process the entries more quickly, perhaps using a separate durable queue for those slow consumers.)
054 * </p>
055 * <h2>Consumers</h2>
056 * <p>
057 * Consumers can be added after the ring buffer has entries, but such consumers will only see those entries that are added after
058 * the consumer has been attached to the buffer. Additionally, the ring buffer guarantees that the consumers will be called from a
059 * single thread, so consumers do <em>not</em> need to be concurrent or thread-safe.
060 * </p>
061 * <h2>Batching</h2>
062 * <p>
063 * Even though there is almost no locking within the ring buffer, the ring buffer uses another technique to make it as fast as
064 * possible: batching. A producer can add multiple entries, called a "batch", at once. So rather than having to check for each
065 * entry the the values that are shared among the different threads, adding entries via a batch means the shared data needs to be
066 * checked only once per batch.
067 * </p>
068 * <p>
069 * The consumer threads also process batches, although most of this is hidden within the runnable that calls the
070 * {@link Consumer#consume(Object, long, long)} method. When ready to process an entry, this code asks for one entry and will get
071 * as many entries that are available. All of the returned entries can then be processed without having to check any of the shared
072 * data.
073 * </p>
074 * <h2>Shutdown</h2>
075 * <p>
076 * The {@link #shutdown()} method is a graceful termination that immediately prevents adding new entries and that allows all
077 * consumer threads to continue processing all previously-added entries. When each thread has consumed all entries, the consumer's
078 * thread will terminate and the consumer "unregistered" from the ring buffer. The method will block until all consumers have
079 * completed and are terminated.
080 * </p>
081 * <p>
082 * Once a ring buffer has been shutdown, it cannot be restarted.
083 * </p>
084 * 
085 * @param <T> the type of entries stored in the buffer
086 * @param <C> the type of consumer
087 * @author Randall Hauch (rhauch@redhat.com)
088 */
089public final class RingBuffer<T, C> {
090
091    private final int bufferSize;
092    private final int mask;
093    protected final Cursor cursor;
094    private final Object[] buffer;
095    private final Executor executor;
096    protected final AtomicBoolean addEntries = new AtomicBoolean(true);
097    protected final ConsumerAdapter<T, C> consumerAdapter;
098    private final Set<ConsumerRunner> consumers = new CopyOnWriteArraySet<>();
099    private final GarbageCollectingConsumer gcConsumer;
100    private final Lock producerLock;
101    protected final Logger logger = Logger.getLogger(getClass());
102
103    RingBuffer( String name,
104                Cursor cursor,
105                Executor executor,
106                ConsumerAdapter<T, C> consumerAdapter,
107                boolean gcEntries,
108                boolean singleProducer ) {
109        this.cursor = cursor;
110        this.bufferSize = cursor.getBufferSize();
111        CheckArg.isPositive(bufferSize, "cursor.getBufferSize()");
112        CheckArg.isPowerOfTwo(bufferSize, "cursor.getBufferSize()");
113        this.mask = bufferSize - 1;
114        this.buffer = new Object[bufferSize];
115        this.executor = executor;
116        this.consumerAdapter = consumerAdapter;
117        if (gcEntries) {
118            this.gcConsumer = this.cursor.createGarbageCollectingConsumer(new Collectable() {
119
120                @Override
121                public void collect( long position ) {
122                    // System.out.println("----  CLEAR " + position);
123                    clearEntry(position);
124                }
125            });
126            this.executor.execute(gcConsumer);
127        } else {
128            this.gcConsumer = null;
129        }
130
131        if (singleProducer) {
132            // There is but one thread calling 'add', so no need for alock. Create an impl that does nothing ...
133            producerLock = new NoOpLock();
134        } else {
135            // Multiple threads can call 'add', so use a real lock ...
136            producerLock = new ReentrantLock();
137        }
138    }
139
140    /**
141     * Add to this buffer a single entry. This method blocks if there is no room in the ring buffer, providing back pressure on
142     * the caller in such cases. Note that if this method blocks for any length of time, that means at least one consumer has yet
143     * to process all of the entries that are currently in the ring buffer. In such cases, consider whether a larger ring buffer
144     * is warranted.
145     * 
146     * @param entry the entry to be added; may not be null
147     * @return true if the entry was added, or false if the buffer has been {@link #shutdown()}
148     */
149    public boolean add( T entry ) {
150        assert entry != null;
151        if (!addEntries.get()) return false;
152        try {
153            producerLock.lock();
154            long position = cursor.claim(); // blocks; if this fails, we will not have successfully claimed and nothing to do ...
155            int index = (int)(position & mask);
156            buffer[index] = entry;
157            return cursor.publish(position);
158        } finally {
159            producerLock.unlock();
160        }
161    }
162
163    /**
164     * Add to this buffer multiple entries. This method blocks until it is added.
165     * 
166     * @param entries the entries that are to be added; may not be null
167     * @return true if all of the entries were added, or false if the buffer has been {@link #shutdown()} and none of the entries
168     *         were added
169     */
170    public boolean add( T[] entries ) {
171        assert entries != null;
172        if (entries.length == 0 || !addEntries.get()) return false;
173        try {
174            producerLock.lock();
175            long position = cursor.claim(entries.length); // blocks
176            for (int i = 0; i != entries.length; ++i) {
177                int index = (int)(position & mask);
178                buffer[index] = entries[i];
179            }
180            return cursor.publish(position);
181        } finally {
182            producerLock.unlock();
183        }
184    }
185
186    @SuppressWarnings( "unchecked" )
187    protected T getEntry( long position ) {
188        if (position < (cursor.getCurrent() - bufferSize)) {
189            // The cursor has already overwritten the entry ...
190            return null;
191        }
192        int index = (int)(position & mask);
193        return (T)buffer[index];
194    }
195
196    protected void clearEntry( long position ) {
197        if (position < (cursor.getCurrent() - bufferSize)) {
198            // The cursor has already overwritten the entry ...
199            return;
200        }
201        int index = (int)(position & mask);
202        buffer[index] = null;
203    }
204
205    /**
206     * Add the supplied consumer, and have it start processing entries in a separate thread.
207     * <p>
208     * Note that the thread will block when there are no more entries to be consumed. If the thread gets a timeout when waiting
209     * for an entry, this method will retry the wait only one time before stopping.
210     * </p>
211     * <p>
212     * The consumer is automatically removed from the ring buffer when it returns {@code false} from its
213     * {@link Consumer#consume(Object, long, long)} method.
214     * </p>
215     * 
216     * @param consumer the component that will process the entries; may not be null
217     * @return true if the consumer was added, or false if the consumer was already registered with this buffer
218     */
219    public boolean addConsumer( final C consumer ) {
220        return addConsumer(consumer, 1);
221    }
222
223    /**
224     * Add the supplied consumer, and have it start processing entries in a separate thread.
225     * <p>
226     * The consumer is automatically removed from the ring buffer when it returns {@code false} from its
227     * {@link Consumer#consume(Object, long, long)} method.
228     * </p>
229     * 
230     * @param consumer the component that will process the entries; may not be null
231     * @param timesToRetryUponTimeout the number of times that the thread should retry after timing out while waiting for the next
232     *        entry; retries will not be attempted if the value is less than 1
233     * @return true if the consumer was added, or false if the consumer was already registered with this buffer
234     * @throws IllegalStateException if the ring buffer has already been {@link #shutdown()}
235     */
236    public boolean addConsumer( final C consumer,
237                                final int timesToRetryUponTimeout ) {
238        if (!addEntries.get()) {
239            throw new IllegalStateException();
240        }
241        ConsumerRunner runner = new ConsumerRunner(consumer, timesToRetryUponTimeout);
242        if (gcConsumer != null) gcConsumer.stayBehind(runner.getPointer());
243
244        // Try to add the runner instance, with equality based upon consumer instance equality ...
245        if (!consumers.add(runner)) return false;
246
247        // It was added, so start it ...
248        executor.execute(runner);
249        return true;
250    }
251
252    /**
253     * Remove the supplied consumer, and block until it stops running and is closed and removed from this buffer. The consumer is
254     * removed at the earliest conevenient point, and will stop seeing entries as soon as it is removed.
255     * 
256     * @param consumer the consumer component to be removed entry; retries will not be attempted if the value is less than 1
257     * @return true if the consumer was removed, stopped, and closed, or false if the supplied consumer was not actually
258     *         registered with this buffer (it may have completed)
259     * @throws IllegalStateException if the ring buffer has already been {@link #shutdown()}
260     */
261    public boolean remove( C consumer ) {
262        if (consumer != null) {
263            // Iterate through the map to find the runner that owns this consumer ...
264            ConsumerRunner match = null;
265            for (ConsumerRunner runner : consumers) {
266                if (runner.getConsumer().equals(consumer)) {
267                    match = runner;
268                    break;
269                }
270            }
271            // Try to remove the matching runner (if we found one) from our list ...
272            if (match != null) {
273                // Tell the thread to stop and wait for it, after which it will have been removed from our map ...
274                match.close();
275                return true;
276            }
277        }
278        // We either didn't find it, or we found it but something else remove it while we searched ...
279        return false;
280    }
281
282    /**
283     * Method called by the {@link ConsumerRunner#run()} method just before the method returns and the thread terminates. This
284     * method invocation allows this buffer to clean up its reference to the runner.
285     * 
286     * @param runner the runner that has completed
287     */
288    protected void disconnect( ConsumerRunner runner ) {
289        this.consumers.remove(runner);
290        if (gcConsumer != null) gcConsumer.ignore(runner.getPointer());
291    }
292
293    protected int getBufferSize() {
294        return bufferSize;
295    }
296
297    /**
298     * Checks if there are any consumers registered.
299     * 
300     * @return {@code true} if this buffer has any consumers, {@code false} otherwise.
301     */
302    public boolean hasConsumers() {
303        return !this.consumers.isEmpty();
304    }
305
306    /**
307     * Shutdown this ring buffer by preventing any further entries, but allowing all existing entries to be processed by all
308     * consumers.
309     */
310    public void shutdown() {
311        // Prevent new entries from being added ...
312        this.addEntries.set(false);
313
314        // Mark the cursor as being finished; this will stop all consumers from waiting for a batch ...
315        this.cursor.complete();
316
317        // Each of the consumer threads will complete the batch they're working on, but will then terminate ...
318
319        // Stop the garbage collection thread (if running) ...
320        if (this.gcConsumer != null) this.gcConsumer.close();
321
322        // Now, block until all the runners have completed ...
323        for (ConsumerRunner runner : new HashSet<>(consumers)) { // use a copy of the runners; they're removed when they close
324            runner.waitForCompletion();
325        }
326        assert consumers.isEmpty();
327    }
328
329    /**
330     * Adapts the {@link #consume(Object, Object, long, long)}, {@link #close(Object)} and
331     * {@link #handleException(Object, Throwable, Object, long, long)} methods to other methods on an unknown type.
332     * 
333     * @param <EntryType> the type of event
334     * @param <ConsumerType> the type of consumer
335     * @author Randall Hauch (rhauch@redhat.com)
336     */
337    public static interface ConsumerAdapter<EntryType, ConsumerType> {
338
339        /**
340         * Consume an entry from the ring buffer. Generally all exceptions should be handled within this method; any exception
341         * thrown will result in the {@link #handleException(Object, Throwable, Object, long, long)} being called.
342         * 
343         * @param consumer the consumer instance that is to consume the event; never null
344         * @param entry the entry; will not be null
345         * @param position the position of the entry within in the ring buffer; this is typically a monotonically-increasing value
346         * @param maxPosition the maximum position of entries in the ring buffer that are being consumed within the same batch;
347         *        this will be greater or equal to {@code position}
348         * @return {@code true} if the consumer should continue processing the next entry, or {@code false} if this consumer is to
349         *         stop processing any more entries (from this or subsequent batches); returning {@code false} provides a way for
350         *         the consumer to signal that it should no longer be used
351         */
352        boolean consume( ConsumerType consumer,
353                         EntryType entry,
354                         long position,
355                         long maxPosition );
356
357        /**
358         * Called by the {@link RingBuffer} when the {@link #consume(Object, Object, long, long)} method returns false, or when
359         * the buffer has been shutdown and the consumer has {@link #consume(Object, Object, long, long) consumed} all entries in
360         * the now-closed buffer.
361         * <p>
362         * This method allows any resources used by the consumer to be cleaned up when no longer needed
363         * </p>
364         * 
365         * @param consumer the consumer instance that is being closed; never null
366         */
367        void close( ConsumerType consumer );
368
369        /**
370         * Handle an exception that was thrown from the {@link #consume(Object, Object, long, long)}.
371         * 
372         * @param consumer the consumer instance that is to consume the event; never null
373         * @param t the exception; never null
374         * @param entry the entry during the consumption of which generated the exception; will not be null
375         * @param position the position of the entry within in the ring buffer; this is typically a monotonically-increasing value
376         * @param maxPosition the maximum position of entries in the ring buffer that are being consumed within the same batch;
377         *        this will be greater or equal to {@code position}
378         */
379        void handleException( ConsumerType consumer,
380                              Throwable t,
381                              EntryType entry,
382                              long position,
383                              long maxPosition );
384    }
385
386    protected class ConsumerRunner implements Runnable {
387        private final C consumer;
388        private final PointerBarrier barrier;
389        private final Pointer pointer;
390        private final int timesToRetryUponTimeout;
391        private final AtomicBoolean runThread = new AtomicBoolean(true);
392        private final CountDownLatch stopLatch = new CountDownLatch(1);
393
394        protected ConsumerRunner( C consumer,
395                                  final int timesToRetryUponTimeout ) {
396            this.consumer = consumer;
397            this.timesToRetryUponTimeout = timesToRetryUponTimeout;
398            // Create a new barrier and a new pointer for consumer ...
399            this.barrier = cursor.newBarrier();
400            this.pointer = cursor.newPointer(); // the cursor will not wrap beyond this pointer
401        }
402
403        protected Pointer getPointer() {
404            return pointer;
405        }
406
407        protected C getConsumer() {
408            return consumer;
409        }
410
411        @Override
412        public int hashCode() {
413            return consumer.hashCode();
414        }
415
416        @Override
417        public boolean equals( Object obj ) {
418            if (this == obj) return true;
419            if (obj instanceof RingBuffer.ConsumerRunner) {
420                @SuppressWarnings( "unchecked" )
421                ConsumerRunner that = (ConsumerRunner)obj;
422                return this.consumer.equals(that.consumer);
423            }
424            return false;
425        }
426
427        public void close() {
428            if (this.runThread.compareAndSet(true, false)) {
429                try {
430                    this.barrier.close();
431                    // Need to wake up any dependent consumers/thread (e.g., garbage collection) ...
432                    cursor.signalConsumers();
433                    this.stopLatch.await();
434                } catch (InterruptedException e) {
435                    // The thread was interrupted ...
436                    Thread.interrupted();
437                    // do nothing ...
438                }
439            }
440        }
441
442        protected void waitForCompletion() {
443            try {
444                stopLatch.await();
445            } catch (InterruptedException e) {
446                // The thread was interrupted ...
447                Thread.interrupted();
448                // do nothing ...
449            }
450        }
451
452        @Override
453        public void run() {
454            boolean consume = true;
455            try {
456                int retry = timesToRetryUponTimeout;
457                while (consume && runThread.get()) {
458                    T entry = null;
459                    long next = pointer.get() + 1L;
460                    try {
461                        // Try to find the next position we can read to ...
462                        long maxPosition = barrier.waitFor(next);
463                        while (next <= maxPosition) {
464                            entry = getEntry(next);
465                            try {
466                                if (!consumerAdapter.consume(consumer, entry, next, maxPosition)) {
467                                    // The consumer is done, so break out of the loop and clean up ...
468                                    consume = false;
469                                    break;
470                                }
471                            } catch (Throwable t) {
472                                consumerAdapter.handleException(consumer, t, entry, next, maxPosition);
473                            }
474                            next = pointer.incrementAndGet() + 1L;
475                            retry = timesToRetryUponTimeout;
476                        }
477                        if (maxPosition < 0) {
478                            // The buffer has been shutdown and there are no more positions, so we're done ...
479                            return;
480                        }
481                    } catch (TimeoutException e) {
482                        // It took too long to wait, but keep trying ...
483                        --retry;
484                        if (retry < 0) {
485                            return;
486                        }
487                    } catch (InterruptedException e) {
488                        // The thread was interrupted ...
489                        Thread.interrupted();
490                        break;
491                    } catch (RuntimeException e) {
492                        // Don't retry this entry, so just advance the pointer and continue ...
493                        pointer.incrementAndGet();
494                    }
495                }
496            } finally {
497                // We are done ...
498                try {
499                    consume = false;
500                    // Tell the cursor to ignore our pointer ...
501                    cursor.ignore(pointer);
502                } finally {
503                    try {
504                        consumerAdapter.close(consumer);
505                    } catch (Throwable t) {
506                        logger.error(t, CommonI18n.errorWhileClosingRingBufferConsumer, consumer, t.getMessage());
507                    } finally {
508                        try {
509                            disconnect(this);
510                        } finally {
511                            stopLatch.countDown();
512                        }
513                    }
514                }
515            }
516        }
517    }
518
519    protected static final class NoOpLock implements Lock {
520        @Override
521        public void lock() {
522        }
523
524        @Override
525        public void unlock() {
526        }
527
528        @Override
529        public void lockInterruptibly() {
530        }
531
532        @Override
533        public boolean tryLock() {
534            return false;
535        }
536
537        @Override
538        public boolean tryLock( long time,
539                                TimeUnit unit ) {
540            return false;
541        }
542
543        @Override
544        public Condition newCondition() {
545            return null;
546        }
547    }
548
549}