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 static org.hamcrest.core.Is.is;
020import static org.junit.Assert.assertEquals;
021import static org.junit.Assert.assertThat;
022import static org.junit.Assert.assertTrue;
023import static org.junit.Assert.fail;
024import java.util.Random;
025import java.util.concurrent.Executor;
026import java.util.concurrent.Executors;
027import org.junit.Before;
028import org.junit.Test;
029import org.modeshape.common.FixFor;
030import org.modeshape.common.statistic.Stopwatch;
031
032/**
033 * @author Randall Hauch (rhauch@redhat.com)
034 */
035public class RingBufferTest {
036    protected static final Random RANDOM = new Random();
037
038    protected volatile boolean print = false;
039    protected volatile boolean slightPausesInConsumers = false;
040
041    @Before
042    public void beforeEach() {
043        print = false;
044    }
045
046    @Test
047    public void shouldBuildWithNoGarbageCollection() {
048        Executor executor = Executors.newCachedThreadPool();
049        RingBuffer<Long, Consumer<Long>> ringBuffer = RingBufferBuilder.withSingleProducer(executor, Long.class).ofSize(8)
050                                                                       .garbageCollect(false).build();
051        print = false;
052
053        // Add 10 entries with no consumers ...
054        long value = 0L;
055        for (int i = 0; i != 10; ++i) {
056            print("Adding entry " + value);
057            ringBuffer.add(value++);
058        }
059
060        // Add a single consumer that should start seeing items 10 and up ...
061        MonotonicallyIncreasingConsumer consumer1 = new MonotonicallyIncreasingConsumer("first", 10L, 10L, 0);
062        ringBuffer.addConsumer(consumer1);
063
064        // Add 10 more entries ...
065        for (int i = 0; i != 10; ++i) {
066            print("Adding entry " + value);
067            ringBuffer.add(value++);
068            // Thread.sleep(100L);
069        }
070
071        ringBuffer.shutdown();
072        print("");
073        print("Ring buffer shutdown completed");
074        assertTrue(consumer1.isClosed());
075    }
076
077    @Test
078    public void shouldBuildWithGarbageCollectionAnd8Entries() {
079        Executor executor = Executors.newCachedThreadPool();
080        RingBuffer<Long, Consumer<Long>> ringBuffer = RingBufferBuilder.withSingleProducer(executor, Long.class).ofSize(8)
081                                                                       .garbageCollect(true).build();
082        print = false;
083
084        // Add 10 entries with no consumers ...
085        long value = 0L;
086        for (int i = 0; i != 10; ++i) {
087            print("Adding entry " + value);
088            ringBuffer.add(value++);
089        }
090
091        // Add a single consumer that should start seeing items 10 and up ...
092        MonotonicallyIncreasingConsumer consumer1 = new MonotonicallyIncreasingConsumer("first", 10L, 10L, 0);
093        ringBuffer.addConsumer(consumer1);
094
095        // Add 10 more entries ...
096        for (int i = 0; i != 10; ++i) {
097            print("Adding entry " + value);
098            ringBuffer.add(value++);
099            // Thread.sleep(100L);
100        }
101
102        ringBuffer.shutdown();
103        print("");
104        print("Ring buffer shutdown completed");
105        // assertTrue(consumer1.isClosed());
106    }
107
108    @Test
109    public void shouldBuildWithGarbageCollectionAnd1024Entries() {
110        Executor executor = Executors.newCachedThreadPool();
111        RingBuffer<Long, Consumer<Long>> ringBuffer = RingBufferBuilder.withSingleProducer(executor, Long.class).ofSize(1024)
112                                                                       .garbageCollect(true).build();
113        print = false;
114
115        // Add 10 entries with no consumers ...
116        long value = 0L;
117        for (int i = 0; i != 10; ++i) {
118            print("Adding entry " + value);
119            ringBuffer.add(value++);
120        }
121
122        // Add a single consumer that should start seeing items 10 and up ...
123        MonotonicallyIncreasingConsumer consumer1 = new MonotonicallyIncreasingConsumer("first", 10L, 10L, 0);
124        ringBuffer.addConsumer(consumer1);
125
126        // Add 10 more entries ...
127        for (int i = 0; i != 1000; ++i) {
128            print("Adding entry " + value);
129            ringBuffer.add(value++);
130            // Thread.sleep(100L);
131        }
132
133        ringBuffer.shutdown();
134        print("");
135        print("Ring buffer shutdown completed");
136        // assertTrue(consumer1.isClosed());
137    }
138
139    @Test
140    public void shouldBeAbleToAddAndRemoveConsumers() throws Exception {
141        Executor executor = Executors.newCachedThreadPool();
142        RingBuffer<Long, Consumer<Long>> ringBuffer = RingBufferBuilder.withSingleProducer(executor, Long.class).ofSize(8)
143                                                                       .build();
144        print = false;
145
146        // Add 10 entries with no consumers ...
147        long value = 0L;
148        for (int i = 0; i != 10; ++i) {
149            print("Adding entry " + value);
150            ringBuffer.add(value++);
151        }
152
153        // Add a single consumer that should start seeing items 10 and up ...
154        MonotonicallyIncreasingConsumer consumer1 = new MonotonicallyIncreasingConsumer("first", 10L, 10L, 0);
155        ringBuffer.addConsumer(consumer1);
156
157        // Add 10 more entries ...
158        for (int i = 0; i != 10; ++i) {
159            print("Adding entry " + value);
160            ringBuffer.add(value++);
161            // Thread.sleep(100L);
162        }
163
164        // Add a second consumer that should start seeing items 20 and up ...
165        MonotonicallyIncreasingConsumer consumer2 = new MonotonicallyIncreasingConsumer("second", 20L, 20L, 0);
166        ringBuffer.addConsumer(consumer2);
167
168        // Add 10 more entries ...
169        for (int i = 0; i != 10; ++i) {
170            print("Adding entry " + value);
171            ringBuffer.add(value++);
172            // Thread.sleep(100L);
173        }
174
175        ringBuffer.remove(consumer2);
176
177        // Add 10 more entries ...
178        for (int i = 0; i != 10; ++i) {
179            print("Adding entry " + value);
180            ringBuffer.add(value++);
181            // Thread.sleep(100L);
182        }
183
184        assertTrue(consumer2.isClosed());
185        ringBuffer.shutdown();
186        print("");
187        print("Ring buffer shutdown completed");
188        assertTrue(consumer1.isClosed());
189        assertTrue(consumer2.isClosed());
190    }
191
192    @Test
193    // @Ignore( "Takes a long time to run" )
194    public void consumersShouldSeeEventsInCorrectOrder() throws Exception {
195        Executor executor = Executors.newCachedThreadPool();
196        RingBuffer<Long, MonotonicallyIncreasingConsumer> ringBuffer = RingBufferBuilder.withSingleProducer(executor,
197                                                                                                            LongConsumerAdapter.INSTANCE)
198                                                                                        .ofSize(8).garbageCollect(false).build();
199        print = false;
200
201        // Add 10 entries with no consumers ...
202        long value = 0L;
203        for (int i = 0; i != 10; ++i) {
204            print("Adding entry " + value);
205            ringBuffer.add(value++);
206        }
207
208        // Add a single consumer that should start seeing items 10 and up ...
209        MonotonicallyIncreasingConsumer consumer1 = new MonotonicallyIncreasingConsumer("first", 10L, 10L, 0);
210        ringBuffer.addConsumer(consumer1);
211
212        // Add 10 more entries ...
213        for (int i = 0; i != 10; ++i) {
214            print("Adding entry " + value);
215            ringBuffer.add(value++);
216            // Thread.sleep(100L);
217        }
218
219        // Add a single consumer that should start seeing items 10 and up ...
220        MonotonicallyIncreasingConsumer consumer2 = new MonotonicallyIncreasingConsumer("second", 20L, 20L, 0);
221        ringBuffer.addConsumer(consumer2);
222
223        // Add 10 more entries ...
224        for (int i = 0; i != 10; ++i) {
225            print("Adding entry " + value);
226            ringBuffer.add(value++);
227            // Thread.sleep(100L);
228        }
229
230        // Add a second consumer that should start seeing items 20 and up ...
231        MonotonicallyIncreasingConsumer consumer3 = new MonotonicallyIncreasingConsumer("third", 30L, 30L, 0);
232        ringBuffer.addConsumer(consumer3);
233
234        // Add 10 more entries ...
235        for (int i = 0; i != 10; ++i) {
236            print("Adding entry " + value);
237            ringBuffer.add(value++);
238            // Thread.sleep(100L);
239        }
240
241        // Add a second consumer that should start seeing items 20 and up ...
242        MonotonicallyIncreasingConsumer consumer4 = new MonotonicallyIncreasingConsumer("fourth", 40L, 40L, 0);
243        ringBuffer.addConsumer(consumer4);
244
245        // Add 10 more entries ...
246        for (int i = 0; i != 10; ++i) {
247            print("Adding entry " + value);
248            ringBuffer.add(value++);
249            // Thread.sleep(100L);
250        }
251
252        // print = true;
253        slightPausesInConsumers = false;
254        boolean slightPauseBetweenEvents = false;
255
256        // Add 400K more entries
257        Stopwatch sw = new Stopwatch();
258        int count = 2000;
259        sw.start();
260        for (int i = 0; i != count; ++i) {
261            ringBuffer.add(value++);
262            if (slightPauseBetweenEvents) {
263                Thread.sleep(RANDOM.nextInt(50));
264            }
265        }
266        sw.stop();
267
268        // Do 10 more while printing ...
269        for (int i = 0; i != 10; ++i) {
270            // print = true;
271            print("Adding entry " + value);
272            ringBuffer.add(value++);
273        }
274
275        ringBuffer.shutdown();
276        print("");
277        print("Ring buffer shutdown completed");
278        assertTrue(consumer1.isClosed());
279        assertTrue(consumer2.isClosed());
280        assertTrue(consumer3.isClosed());
281        assertTrue(consumer4.isClosed());
282
283        --value;
284        assertThat(consumer1.getLastValue(), is(value));
285        assertThat(consumer2.getLastValue(), is(value));
286        assertThat(consumer3.getLastValue(), is(value));
287        assertThat(consumer4.getLastValue(), is(value));
288
289        print("");
290        print("Time to add " + count + " entries: " + sw.getAverageDuration());
291    }
292    
293    @Test
294    @FixFor( "MODE-2195" )
295    public void shouldAutomaticallySetTheBufferSizeToTheNextPowerOf2() throws Exception {
296        Executor executor = Executors.newCachedThreadPool();
297        RingBuffer<Long, Consumer<Long>> ringBuffer = RingBufferBuilder.withSingleProducer(executor, Long.class).ofSize(5)
298                                                                       .garbageCollect(false).build();    
299        assertEquals(8, ringBuffer.getBufferSize());
300
301        ringBuffer = RingBufferBuilder.withSingleProducer(executor, Long.class).ofSize(1023).garbageCollect(false).build();
302        assertEquals(1024, ringBuffer.getBufferSize());
303    }
304
305    protected void print( String message ) {
306        if (print) System.out.println(message);
307    }
308
309    protected class MonotonicallyIncreasingConsumer extends Consumer<Long> {
310        private final String id;
311        private boolean first = true;
312        private long lastValue = -1L;
313        private long lastPosition = -1L;
314        private boolean closed = false;
315        private final int secondsToWork;
316
317        public MonotonicallyIncreasingConsumer( String id,
318                                                long firstValue,
319                                                long firstPosition,
320                                                int secondsToWork ) {
321            this.id = id;
322            this.lastValue = firstValue;
323            this.lastPosition = firstPosition;
324            this.secondsToWork = secondsToWork;
325        }
326
327        @Override
328        public boolean consume( Long entry,
329                                long position,
330                                long max ) {
331            assertTrue(!closed);
332            print(id + " consuming " + entry.longValue() + " at position " + position + " with max " + max);
333            try {
334                Thread.sleep(secondsToWork * 1000);
335            } catch (Exception e) {
336                e.printStackTrace();
337                fail(e.getMessage());
338            }
339            if (slightPausesInConsumers && position % 1000 == 0) {
340                try {
341                    Thread.sleep(RANDOM.nextInt(100));
342                } catch (Exception e) {
343                    e.printStackTrace();
344                    fail(e.getMessage());
345                }
346            }
347            if (first) {
348                assertTrue(entry.longValue() == lastValue);
349                assertTrue(position == lastPosition);
350                first = false;
351            } else {
352                assertTrue(entry.longValue() == (lastValue + 1));
353                lastValue = entry.longValue();
354                assertTrue(position == (lastPosition + 1));
355                lastPosition = position;
356            }
357            return true;
358        }
359
360        @Override
361        public void close() {
362            super.close();
363            print(id + " closing");
364            closed = true;
365        }
366
367        public long getLastPosition() {
368            return lastPosition;
369        }
370
371        public long getLastValue() {
372            return lastValue;
373        }
374
375        public boolean isClosed() {
376            return closed;
377        }
378    }
379
380    private static class LongConsumerAdapter implements RingBuffer.ConsumerAdapter<Long, MonotonicallyIncreasingConsumer> {
381        protected static final LongConsumerAdapter INSTANCE = new LongConsumerAdapter();
382
383        private LongConsumerAdapter() {
384        }
385
386        @Override
387        public boolean consume( MonotonicallyIncreasingConsumer consumer,
388                                Long event,
389                                long position,
390                                long maxPosition ) {
391            consumer.consume(event, position, maxPosition);
392            return true;
393        }
394
395        @Override
396        public void close( MonotonicallyIncreasingConsumer consumer ) {
397            consumer.close();
398        }
399
400        @Override
401        public void handleException( MonotonicallyIncreasingConsumer consumer,
402                                     Throwable t,
403                                     Long entry,
404                                     long position,
405                                     long maxPosition ) {
406            throw new AssertionError("Test failure", t);
407        }
408    }
409
410}