/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.toolkit.modules.concurrency.internal; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.easymock.EasyMock; import org.junit.After; import org.junit.Before; import org.junit.Test; import de.rcenvironment.toolkit.modules.concurrency.api.BatchAggregator; import de.rcenvironment.toolkit.modules.concurrency.api.BatchProcessor; import de.rcenvironment.toolkit.modules.concurrency.api.ConcurrencyUtilsFactory; import de.rcenvironment.toolkit.utils.internal.StringUtils; /** * Unit test for {@link BatchAggregatorImpl}. * * @author Robert Mischke */ public class BatchAggregatorImplTest extends AbstractConcurrencyModuleTest { private static final String DUMMY_STRING = "dummy"; private static final int SIMULATE_BLOCKING_PROCESSOR_WAIT_MSEC = 500; /** * A dummy receiver for generated batches. Also provides a Semaphore for convenient waiting. * * @param <T> the element type of the batches; * * @author Robert Mischke */ private class CollectingBatchProcessor<T> implements BatchProcessor<T> { public List<List<T>> receivedBatches = new ArrayList<List<T>>(); public Semaphore receiveCountSemaphore = new Semaphore(0); @Override public void processBatch(List<T> batch) { receivedBatches.add(batch); receiveCountSemaphore.release(); } } /** * A batch processor that throws a {@link RuntimeException} from {@link #processBatch(List)}. * * @author Robert Mischke */ private static final class FailingBatchProcessor implements BatchProcessor<String> { @Override public void processBatch(List<String> batch) { LogFactory.getLog(getClass()).debug( "Simulating processor failure for element " + batch.get(0) + "; expecting a single follow-up stacktrace"); // throw arbitrary RuntimeException throw new IllegalStateException("Simulated processor failure for element " + batch.get(0)); } } // arbitrary "short wait" length private static final int SHORT_WAIT_MSEC = 100; // the timeout of the "blocking callback" test; extracted for CheckStyle private static final int BLOCKING_TEST_TIMEOUT = 1000; private Log originalLogger; private Log mockLogger; private Log unitTestLogger = LogFactory.getLog(getClass()); /** * Test setup. */ @Before public void setUp() { originalLogger = BatchAggregatorImpl.getLogger(); mockLogger = EasyMock.createMock(Log.class); BatchAggregatorImpl.setLogger(mockLogger); unitTestLogger.debug("Replaced BatchAggregator logger with mock"); } /** * Test teardown. */ @After public void tearDown() { unitTestLogger.debug("Restoring BatchAggregator logger"); BatchAggregatorImpl.setLogger(originalLogger); } /** * Test concurrent writing to a single aggregator, and whether the posted messages are in the correct order for each posting thread. * * @throws InterruptedException on Thread interruption */ @Test public void testMultiThreadSingleBatchPosting() throws InterruptedException { final int numThreads = 10; final int numMsgPerThread = 500; final int maxLatency = 10 * 1000; // test should finish in 10 secs final AtomicInteger threadIdGenerator = new AtomicInteger(); final CollectingBatchProcessor<String> collector = new CollectingBatchProcessor<String>(); final BatchAggregator<String> aggregator = getConcurrencyUtilsFactory().createBatchAggregator(numThreads * numMsgPerThread, maxLatency, collector); ExecutorService executor = Executors.newFixedThreadPool(10); Runnable threadRunnable = new Runnable() { @Override public void run() { int threadId = threadIdGenerator.incrementAndGet(); for (int i = 1; i <= numMsgPerThread; i++) { aggregator.enqueue(StringUtils.format("Thread %d, Msg %d", threadId, i)); } } }; for (int t = 1; t <= numThreads; t++) { executor.execute(threadRunnable); } collector.receiveCountSemaphore.acquire(); List<String> batch = collector.receivedBatches.get(0); assertEquals(numThreads * numMsgPerThread, batch.size()); // test per-thread ordering of messages Pattern p = Pattern.compile("Thread (\\d+), Msg (\\d+)"); for (int t = 1; t <= numThreads; t++) { String threadId = Integer.toString(t); int lastMsg = 0; // not arbitrary; must be 1 less than 1st message for (String element : batch) { Matcher matcher = p.matcher(element); if (!matcher.find()) { throw new IllegalArgumentException(element); } if (matcher.group(1).equals(threadId)) { int msgId = Integer.parseInt(matcher.group(2)); assertEquals("Message out of order: " + element, lastMsg + 1, msgId); lastMsg = msgId; } } assertEquals("Received less messages per thread than expected", numMsgPerThread, lastMsg); } } /** * Tests whether batches are properly sent out on maximum latency timeout (but not earlier). * * @throws InterruptedException on Thread interruption */ @Test public void testBatchSendingByMaxLatency() throws InterruptedException { final CollectingBatchProcessor<String> collector = new CollectingBatchProcessor<String>(); final int maxLatency = 300; final BatchAggregator<String> aggregator = getConcurrencyUtilsFactory().createBatchAggregator(Integer.MAX_VALUE, maxLatency, collector); aggregator.enqueue("latency.msg1"); assertEquals("Message 1 received before timer should have elapsed", 0, collector.receivedBatches.size()); Thread.sleep(maxLatency * 2); assertEquals("No batch received after timer should have elapsed", 1, collector.receivedBatches.size()); // the old batch must have ended, so this message should trigger a new batch // (and therefore, a new timer) aggregator.enqueue("latency.msg2"); assertEquals("Message 2 received before timer should have elapsed", 1, collector.receivedBatches.size()); Thread.sleep(maxLatency * 2); assertEquals("No batch received after timer should have elapsed", 2, collector.receivedBatches.size()); // check consistency assertEquals("latency.msg1", collector.receivedBatches.get(0).get(0)); assertEquals("latency.msg2", collector.receivedBatches.get(1).get(0)); } /** * Tests whether batches are properly sent out when max size is reached. * * @throws InterruptedException on Thread interruption */ @Test public void testSizeLimiting() throws InterruptedException { final int numBatches = 20; final int maxBatchSize = 5; final int numMessages = maxBatchSize * numBatches; final int maxLatency = 200; final CollectingBatchProcessor<String> collector = new CollectingBatchProcessor<String>(); final BatchAggregator<String> aggregator = getConcurrencyUtilsFactory().createBatchAggregator(maxBatchSize, maxLatency, collector); for (int i = 1; i <= numMessages; i++) { aggregator.enqueue("msg" + i); } // let timers finish Thread.sleep(maxLatency * 3); assertEquals("Wrong number of batches from size limiting", numBatches, collector.receivedBatches.size()); // check consistency (some samples) assertEquals("msg1", collector.receivedBatches.get(0).get(0)); assertEquals("msg5", collector.receivedBatches.get(0).get(maxBatchSize - 1)); assertEquals("msg96", collector.receivedBatches.get(numBatches - 1).get(0)); assertEquals("msg100", collector.receivedBatches.get(numBatches - 1).get(maxBatchSize - 1)); } /** * Verifies that elements can still be added after the processor failed for a batch triggered by maximum size. * * @throws InterruptedException on test interruption */ @Test public void testFailingBatchProcessorWithSizeBatching() throws InterruptedException { BatchProcessor<String> failingProcessor = new FailingBatchProcessor(); final int unusedMaxLatency = 100; BatchAggregator<String> aggregator = getConcurrencyUtilsFactory().createBatchAggregator(1, unusedMaxLatency, failingProcessor); // expect two errors to be logged mockLogger.error(EasyMock.anyObject(), (Throwable) EasyMock.anyObject()); mockLogger.error(EasyMock.anyObject(), (Throwable) EasyMock.anyObject()); EasyMock.replay(mockLogger); // send first element; this triggers a batch to process aggregator.enqueue("String 1"); // wait a moment, just to be sure Thread.sleep(SHORT_WAIT_MSEC); // check that elements can still be added aggregator.enqueue("String 2"); // wait a moment so the aggregator thread has finished Thread.sleep(SHORT_WAIT_MSEC); EasyMock.verify(mockLogger); } /** * Verifies that elements can still be added after the processor failed for a batch triggered by maximum latency. * * @throws InterruptedException on test interruption */ @Test public void testFailingBatchProcessorWithTimeBatching() throws InterruptedException { BatchProcessor<String> failingProcessor = new FailingBatchProcessor(); final int unusedMaxBatchSize = 50; final int maxLatency = 100; final int shortTimeWait = 500; BatchAggregator<String> aggregator = getConcurrencyUtilsFactory().createBatchAggregator(unusedMaxBatchSize, maxLatency, failingProcessor); // expect two errors to be logged mockLogger.error(EasyMock.anyObject(), (Throwable) EasyMock.anyObject()); mockLogger.error(EasyMock.anyObject(), (Throwable) EasyMock.anyObject()); EasyMock.replay(mockLogger); // send first element aggregator.enqueue("String 3"); // wait until the timer triggers a batch to process Thread.sleep(shortTimeWait); // check that elements can still be added aggregator.enqueue("String 4"); // wait so possible "late" timer failures can surface Thread.sleep(shortTimeWait); EasyMock.verify(mockLogger); } /** * Verifies that a blocking callback handler does not interfere with simultaneous enqueue() calls. * * @throws InterruptedException on thread interruption */ @Test(timeout = BLOCKING_TEST_TIMEOUT) public void testBlockingCallbackHandler() throws InterruptedException { final CountDownLatch callbackBlocker = new CountDownLatch(1); // create an aggregator with batch size 1, so each enqueue() should trigger a callback final ConcurrencyUtilsFactory factory = getConcurrencyUtilsFactory(); BatchAggregator<String> aggregator = factory.createBatchAggregator(1, Integer.MAX_VALUE, new BatchProcessor<String>() { @Override public void processBatch(List<String> batch) { unitTestLogger.debug("Blocking callback method..."); try { callbackBlocker.await(); } catch (InterruptedException e) { unitTestLogger.warn("Callback thread interrupted", e); } unitTestLogger.debug("Leaving callback method"); } }); aggregator.enqueue(DUMMY_STRING); unitTestLogger.debug("Enqueued object 1; object 2 should follow immediately"); // this is the call that would "hang" on failure aggregator.enqueue("dummy2"); unitTestLogger.debug("Enqueued object 2"); // wait briefly for possible threading effects Thread.sleep(SHORT_WAIT_MSEC); // signal to "release" the callback method callbackBlocker.countDown(); } /** * Tests that concurrent processBatch() methods of different {@link BatchProcessor}s do not block each other. * * @throws InterruptedException on test interruption */ @Test public void testNonBlockingConcurrentDispatch() throws InterruptedException { int n = 10; final CountDownLatch cdl = new CountDownLatch(n); for (int i = 0; i < n; i++) { final int i2 = i; BatchAggregator<String> aggregator = getConcurrencyUtilsFactory().createBatchAggregator(1, 1, new BatchProcessor<String>() { @Override public void processBatch(List<String> batch) { try { Thread.sleep(SIMULATE_BLOCKING_PROCESSOR_WAIT_MSEC); assertEquals(1, batch.size()); String element = batch.get(0); assertEquals(DUMMY_STRING + i2, element); cdl.countDown(); } catch (InterruptedException e) { e.hashCode(); // dummy operation to satisfy CheckStyle } } }); aggregator.enqueue(DUMMY_STRING + i); } boolean allFinished = cdl.await(SIMULATE_BLOCKING_PROCESSOR_WAIT_MSEC * 2, TimeUnit.MILLISECONDS); assertTrue("Concurrent batch processors did not finish in time (probably not running in parallel); remaining=" + cdl.getCount(), allFinished); } }