/* * Copyright (c) MuleSoft, Inc. All rights reserved. http://www.mulesoft.com * The software in this package is published under the terms of the CPAL v1.0 * license, a copy of which has been included with this distribution in the * LICENSE.txt file. */ package org.mule.runtime.core.internal.streaming.object; import static org.mule.test.allure.AllureConstants.StreamingFeature.STREAMING; import static org.mule.test.allure.AllureConstants.StreamingFeature.StreamingStory.OBJECT_STREAMING; import static java.lang.Math.toIntExact; import static java.util.concurrent.Executors.newScheduledThreadPool; import static java.util.concurrent.TimeUnit.SECONDS; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.Assert.assertThat; import org.mule.runtime.api.streaming.exception.StreamingBufferSizeExceededException; import org.mule.runtime.api.streaming.object.CursorIterator; import org.mule.runtime.api.streaming.object.CursorIteratorProvider; import org.mule.runtime.core.streaming.object.InMemoryCursorIteratorConfig; import org.mule.runtime.core.util.func.CheckedConsumer; import org.mule.runtime.core.util.func.CheckedRunnable; import org.mule.tck.size.SmallTest; import java.io.IOException; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.NoSuchElementException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import ru.yandex.qatools.allure.annotations.Description; import ru.yandex.qatools.allure.annotations.Features; import ru.yandex.qatools.allure.annotations.Stories; @RunWith(Parameterized.class) @SmallTest @Features(STREAMING) @Stories(OBJECT_STREAMING) public class CursorIteratorProviderTestCase extends AbstractObjectStreamingTestCase { protected static final int DATA_SIZE = 500; @Parameterized.Parameters(name = "{0}") public static Collection<Object[]> data() { return Arrays.asList(new Object[][] { {"Doesn't require expansion", DATA_SIZE, DATA_SIZE, 0, DATA_SIZE}, {"Requires expansion", DATA_SIZE, 100, 50, DATA_SIZE} }); } private final int halfDataLength; private final InMemoryCursorIteratorConfig config; protected final ScheduledExecutorService executorService; private CursorIteratorProvider streamProvider; private CountDownLatch controlLatch; private CountDownLatch mainThreadLatch; public CursorIteratorProviderTestCase(String name, int dataSize, int initialBufferSize, int bufferSizeIncrement, int maxBufferSize) { super(dataSize); this.config = new InMemoryCursorIteratorConfig(initialBufferSize, bufferSizeIncrement, maxBufferSize); executorService = newScheduledThreadPool(2); halfDataLength = data.size() / 2; resetLatches(); } @Before public void before() { streamProvider = createStreamProvider(data); } protected CursorIteratorProvider createStreamProvider(List<Object> data) { return new InMemoryCursorIteratorProvider(toStreamingIterator(data), config); } @After public void after() { streamProvider.close(); executorService.shutdownNow(); } @Test @Description("fully consume stream in a single thread") public void readFullyWithInSingleCursor() throws IOException { withCursor(cursor -> checkEquals(data, cursor)); } @Test @Description("Partially consume the stream, rewind back to zero and consume fully") public void rewindWhileStreamNotFullyConsumed() throws Exception { withCursor(cursor -> { List<Object> read = read(cursor, halfDataLength); checkEquals(read, data.subList(0, halfDataLength)); cursor.seek(0); read = read(cursor, data.size()); checkEquals(read, data); }); } @Test @Description("Consume the stream, go back to two different positions and consume again (each)") public void randomSeekWithOneOpenCursor() throws Exception { withCursor(cursor -> { // read fully checkEquals(data, cursor); // go back and read first 10 items seekAndAssert(cursor, 0, 10); // move to the middle and read the rest seekAndAssert(cursor, halfDataLength, halfDataLength); }); } @Test @Description("Two open cursors consume the same stream, one after the other in the same thread") public void twoOpenCursorsConsumingTheStreamInSingleThread() throws Exception { withCursor(cursor1 -> withCursor(cursor2 -> { seekAndAssert(cursor1, 0, data.size()); seekAndAssert(cursor2, 0, data.size()); })); } @Test @Description("Two open cursors consume different ends of the same stream, one after the other in the same thread") public void twoOpenCursorsReadingOppositeEndsOfTheStreamInSingleThread() throws Exception { withCursor(cursor1 -> withCursor(cursor2 -> { seekAndAssert(cursor1, 0, data.size() / 2); seekAndAssert(cursor2, halfDataLength, halfDataLength); })); } @Test @Description("Two open cursors consume the same stream concurrently, each on its own thread") public void twoOpenCursorsConsumingTheStreamConcurrently() throws Exception { withCursor(cursor1 -> withCursor(cursor2 -> doAsync(() -> seekAndAssert(cursor1, 0, data.size()), () -> seekAndAssert(cursor2, 0, data.size())))); } @Test @Description("Two open cursors consume different ends of same stream concurrently, each on its own thread") public void twoOpenCursorsReadingOppositeEndsOfTheStreamConcurrently() throws Exception { withCursor(cursor1 -> withCursor(cursor2 -> doAsync(() -> seekAndAssert(cursor1, 0, data.size() / 2), () -> seekAndAssert(cursor2, halfDataLength, halfDataLength)))); } @Test @Description("Seek different positions and verify that getPosition() is consistent") public void getPosition() throws Exception { withCursor(cursor -> { assertThat(cursor.getPosition(), is(0L)); cursor.seek(10); assertThat(cursor.getPosition(), is(10L)); cursor.seek(0); assertThat(cursor.getPosition(), is(0L)); }); } @Test @Description("Get the size of a stream") public void size() throws Exception { withCursor(cursor -> assertThat(cursor.getSize(), is(data.size()))); } @Test(expected = StreamingBufferSizeExceededException.class) @Description("Exceed the maxBufferSize and expect exception") public void bufferSizeExceeded() throws Exception { data.add("I don't fit"); streamProvider.close(); streamProvider = createStreamProvider(data); withCursor(cursor -> read(cursor, data.size())); } @Test @Description("Direct access to the last two items of the stream without traversing the whole cursor") public void getLastTwoItems() throws Exception { withCursor(cursor -> { int size = data.size(); cursor.seek(size - 2); assertThat(cursor.hasNext(), is(true)); assertThat(cursor.next(), is(data.get(size - 2))); assertThat(cursor.next(), is(data.get(size - 1))); assertThat(cursor.hasNext(), is(false)); }); } @Test(expected = NoSuchElementException.class) @Description("Move the cursor to a non existing position") public void outOfBoundsHasNext() throws Exception { withCursor(cursor -> { cursor.seek(data.size() + 100); assertThat(cursor.hasNext(), is(false)); cursor.next(); }); } private void doAsync(CheckedRunnable task1, CheckedRunnable task2) throws Exception { resetLatches(); Future future1 = doAsync(() -> { controlLatch.await(); task1.run(); mainThreadLatch.countDown(); }); Future future2 = doAsync(() -> { controlLatch.countDown(); task2.run(); mainThreadLatch.countDown(); }); awaitMainThreadLatch(); assertThat(future1.get(), is(nullValue())); assertThat(future2.get(), is(nullValue())); } private Future doAsync(CheckedRunnable task) { return executorService.submit(() -> { try { task.run(); } catch (Exception e) { throw new RuntimeException(e); } }); } private void awaitMainThreadLatch() throws InterruptedException { mainThreadLatch.await(1, SECONDS); } private void seekAndAssert(CursorIterator<Object> cursor, long position, int size) throws Exception { cursor.seek(position); List<Object> read = read(cursor, size); checkEquals(read, data.subList(toIntExact(position), toIntExact(position + size))); } private void resetLatches() { controlLatch = new CountDownLatch(1); mainThreadLatch = new CountDownLatch(2); } private void withCursor(CheckedConsumer<CursorIterator> consumer) throws IOException { try (CursorIterator cursor = streamProvider.openCursor()) { consumer.accept(cursor); } } }