/* Copyright (C) SYSTAP, LLC DBA Blazegraph 2006-2016. All rights reserved. Contact: SYSTAP, LLC DBA Blazegraph 2501 Calvert ST NW #106 Washington, DC 20008 licenses@blazegraph.com This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /* * Created on Sep 10, 2008 */ package com.bigdata.relation.accesspath; import java.util.NoSuchElementException; import java.util.concurrent.BlockingDeque; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingDeque; 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.ReentrantLock; import junit.framework.TestCase2; import com.bigdata.util.DaemonThreadFactory; /** * Test suite for {@link BlockingBuffer} and its {@link IAsynchronousIterator} * when using an array type for the elements (chunk processing) and a * {@link BlockingDeque}, which permits combination of chunks as they are added * to the buffer. * * @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a> * @version $Id$ * * @todo test ordered chunk process also. */ public class TestBlockingBufferWithChunksDeque extends TestCase2 { /** * */ public TestBlockingBufferWithChunksDeque() { } /** * @param arg0 */ public TestBlockingBufferWithChunksDeque(String arg0) { super(arg0); } private final ExecutorService service = Executors .newCachedThreadPool(DaemonThreadFactory.defaultThreadFactory()); protected void tearDown() throws Exception { service.shutdownNow(); super.tearDown(); } /** * Basic test of the ability to add to a buffer with a fixed capacity queue * and to drain the elements from the queue including tests of the * non-blocking aspects of the API. * * @throws TimeoutException * @throws ExecutionException * @throws InterruptedException */ public void test_blockingBuffer() throws InterruptedException, ExecutionException, TimeoutException { final Integer e0 = new Integer(0); final Integer e1 = new Integer(1); final Integer e2 = new Integer(2); final Integer e3 = new Integer(3); final Integer e4 = new Integer(4); final Integer e5 = new Integer(5); final Integer e6 = new Integer(6); final int queueCapacity = 3; final BlockingQueue<Integer[]> queue = new LinkedBlockingDeque<Integer[]>( queueCapacity); final int minimumChunkSize = 2; final long chunkTimeout = 1000; final TimeUnit chunkTimeoutUnit = TimeUnit.MILLISECONDS; /* * The test timeout is just a smidge longer than the chunk timeout. * * Note: use Long.MAX_VALUE iff debugging. */ final long testTimeout = Long.MAX_VALUE; // final long testTimeout = chunkTimeout + 20; final boolean ordered = false; final BlockingBuffer<Integer[]> buffer = new BlockingBuffer<Integer[]>( queue, minimumChunkSize, chunkTimeout, chunkTimeoutUnit, ordered); // buffer is empty. assertTrue("isOpen", buffer.isOpen()); assertTrue("isEmpty", buffer.isEmpty()); assertEquals("size", 0, buffer.size()); assertEquals("chunkCount", 0L, buffer.getChunksAddedCount()); assertEquals("elementCount", 0L, buffer.getElementsAddedCount()); final IAsynchronousIterator<Integer[]> itr = buffer.iterator(); // nothing available from the iterator (non-blocking test). assertFalse(itr.hasNext(1, TimeUnit.NANOSECONDS)); assertNull(itr.next(1, TimeUnit.NANOSECONDS)); // add an element to the buffer - should not block. buffer.add(new Integer[]{e0}); // should be one element and one chunk accepted by the buffer. assertTrue("isOpen", buffer.isOpen()); assertFalse("isEmpty", buffer.isEmpty()); assertEquals("size", 1, buffer.size()); assertEquals("chunkCount", 1L, buffer.getChunksAddedCount()); assertEquals("elementCount", 1L, buffer.getElementsAddedCount()); /* * Note: we can not test itr.hasNext() without removing the chunk from * the buffer, in which case the next chunk would not be combined with * the first. */ // // something should be available now (non-blocking). // assertTrue(itr.hasNext(1, TimeUnit.NANOSECONDS)); // // // something should be available now (blocking). // assertTrue(itr.hasNext()); // add another element to the buffer - should not block. buffer.add(new Integer[]{e1}); /* * Should be two elements but only one chunks accepted into the buffer * since the 2nd chunk should have been combined with the first on add. */ assertTrue("isOpen", buffer.isOpen()); assertFalse("isEmpty", buffer.isEmpty()); assertEquals("size", 1, buffer.size()); assertEquals("chunkCount", 2L, buffer.getChunksAddedCount()); assertEquals("elementCount", 2L, buffer.getElementsAddedCount()); /* * Add another element to the buffer. There is only one chunk in the * buffer and it has a length of 2. The new chunk has a length of 3 so * it can not be combined and will result in 2 chunks in the buffer. * The total element count is now 5 := (1 + 1) + 3. */ buffer.add(new Integer[]{e2,e3,e4}); assertTrue("isOpen", buffer.isOpen()); assertFalse("isEmpty", buffer.isEmpty()); assertEquals("size", 2, buffer.size()); assertEquals("chunkCount", 3L, buffer.getChunksAddedCount()); assertEquals("elementCount", 5L, buffer.getElementsAddedCount()); final ReentrantLock lock = new ReentrantLock(); final Condition cond = lock.newCondition(); final AtomicBoolean proceedFlag = new AtomicBoolean(false); // future of task writing another element on the buffer. final Future producerFuture = service.submit(new Callable<Void>() { public Void call() throws Exception { lock.lockInterruptibly(); try { if (!proceedFlag.get()) { cond.await(); } if(log.isInfoEnabled()) log.info("Producer resumed."); assertTrue("isOpen",buffer.isOpen()); assertTrue("isEmpty",buffer.isEmpty()); /* * Add another element - should not block since the buffer is * empty. */ if(log.isInfoEnabled()) log.info("Adding last chunk to buffer."); buffer.add(new Integer[] { e5, e6 }); if(log.isInfoEnabled()) log.info("Added last chunk to buffer."); assertTrue("isOpen", buffer.isOpen()); assertEquals("chunkCount", 4L, buffer.getChunksAddedCount()); assertEquals("elementCount", 7L, buffer.getElementsAddedCount()); /* * itr.hasNext() (in the producer thread) will block until * the buffer is closed. */ buffer.close(); } finally { lock.unlock(); } // done. return null; } }); // future of task draining the buffer. final Future consumerFuture = service.submit(new Callable<Void>() { public Void call() throws Exception { try { lock.lockInterruptibly(); try { assertTrue(itr.hasNext()); // take the first chunk - two elements. if (log.isInfoEnabled()) log.info("Awaiting first chunk"); assertSameArray(new Integer[] { e0, e1 }, itr.next(50, TimeUnit.MILLISECONDS)); if (log.isInfoEnabled()) log.info("Have first chunk"); /* * Verify that we obtained the first chunk before the * buffer was closed. Otherwise next() blocked * attempting to compile a full chunk until the producer * timeout, at which point the producer closed the * buffer and next() noticed the closed buffer and * returned. */ assertTrue(buffer.isOpen()); assertFalse("buffer closed?", itr.isExhausted()); // take the 2nd chunk - 3 elements. if (log.isInfoEnabled()) log.info("Awaiting second chunk"); assertSameArray(new Integer[] { e2, e3, e4 }, itr.next()); if (log.isInfoEnabled()) log.info("Have second chunk"); /* * Verify that nothing is available from the iterator * (non-blocking test). */ assertFalse(itr.hasNext(1, TimeUnit.NANOSECONDS)); assertNull(itr.next(1, TimeUnit.NANOSECONDS)); // Signal the producer that it should continue. proceedFlag.set(true); cond.signal(); } finally { lock.unlock(); } /* * Should block until we close the buffer. Will report * [false] if the consumer was interrupted within hasNext() * since that will cause the iterator to be asynchronously * closed. */ assertTrue("Iterator exhausted?", itr.hasNext()); // last chunk assertSameArray(new Integer[] { e5, e6 }, itr.next()); // should be immediately false. assertFalse(itr.hasNext(1, TimeUnit.NANOSECONDS)); // should be immediately null. assertNull(itr.next(1, TimeUnit.NANOSECONDS)); // The synchronous API should also report an exhausted // itr. assertFalse(itr.hasNext()); try { itr.next(); fail("Expecting: " + NoSuchElementException.class); } catch (NoSuchElementException ex) { if (log.isInfoEnabled()) log.info("Ignoring expected exception: " + ex); } return null; } catch (Throwable t) { log.error("Consumer failed or blocked: " + t, t); throw new Exception(t); } } }); Throwable cause = null; try { // wait a little bit for the producer future. producerFuture.get(testTimeout, chunkTimeoutUnit); } catch(Throwable t) { cause = t; log.error(t,t); } try { // wait a little bit for the consumer future. consumerFuture.get(testTimeout, chunkTimeoutUnit); } catch(Throwable t) { if(cause==null) { cause = t; } log.error(t,t); } if (cause != null) { fail(cause.getLocalizedMessage(), cause); } } }