|
@@ -0,0 +1,804 @@
|
|
|
+/*
|
|
|
+ * Licensed to the Apache Software Foundation (ASF) under one
|
|
|
+ * or more contributor license agreements. See the NOTICE file
|
|
|
+ * distributed with this work for additional information
|
|
|
+ * regarding copyright ownership. The ASF licenses this file
|
|
|
+ * to you under the Apache License, Version 2.0 (the
|
|
|
+ * "License"); you may not use this file except in compliance
|
|
|
+ * with the License. You may obtain a copy of the License at
|
|
|
+ *
|
|
|
+ * http://www.apache.org/licenses/LICENSE-2.0
|
|
|
+ *
|
|
|
+ * Unless required by applicable law or agreed to in writing, software
|
|
|
+ * distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
+ * See the License for the specific language governing permissions and
|
|
|
+ * limitations under the License.
|
|
|
+ */
|
|
|
+
|
|
|
+package org.apache.hadoop.fs.impl;
|
|
|
+
|
|
|
+import java.io.EOFException;
|
|
|
+import java.io.IOException;
|
|
|
+import java.nio.ByteBuffer;
|
|
|
+import java.nio.IntBuffer;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Optional;
|
|
|
+import java.util.concurrent.CompletableFuture;
|
|
|
+import java.util.function.IntFunction;
|
|
|
+
|
|
|
+import org.assertj.core.api.Assertions;
|
|
|
+import org.assertj.core.api.ListAssert;
|
|
|
+import org.assertj.core.api.ObjectAssert;
|
|
|
+import org.junit.Test;
|
|
|
+import org.mockito.ArgumentMatchers;
|
|
|
+import org.mockito.Mockito;
|
|
|
+
|
|
|
+import org.apache.hadoop.fs.ByteBufferPositionedReadable;
|
|
|
+import org.apache.hadoop.fs.FileRange;
|
|
|
+import org.apache.hadoop.fs.PositionedReadable;
|
|
|
+import org.apache.hadoop.fs.VectoredReadUtils;
|
|
|
+import org.apache.hadoop.test.HadoopTestBase;
|
|
|
+
|
|
|
+import static java.util.Arrays.asList;
|
|
|
+import static org.apache.hadoop.fs.FileRange.createFileRange;
|
|
|
+import static org.apache.hadoop.fs.VectoredReadUtils.isOrderedDisjoint;
|
|
|
+import static org.apache.hadoop.fs.VectoredReadUtils.mergeSortedRanges;
|
|
|
+import static org.apache.hadoop.fs.VectoredReadUtils.readRangeFrom;
|
|
|
+import static org.apache.hadoop.fs.VectoredReadUtils.readVectored;
|
|
|
+import static org.apache.hadoop.fs.VectoredReadUtils.sortRanges;
|
|
|
+import static org.apache.hadoop.fs.VectoredReadUtils.validateAndSortRanges;
|
|
|
+import static org.apache.hadoop.test.LambdaTestUtils.intercept;
|
|
|
+import static org.apache.hadoop.test.MoreAsserts.assertFutureCompletedSuccessfully;
|
|
|
+import static org.apache.hadoop.test.MoreAsserts.assertFutureFailedExceptionally;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Test behavior of {@link VectoredReadUtils}.
|
|
|
+ */
|
|
|
+public class TestVectoredReadUtils extends HadoopTestBase {
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Test {@link VectoredReadUtils#sliceTo(ByteBuffer, long, FileRange)}.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testSliceTo() {
|
|
|
+ final int size = 64 * 1024;
|
|
|
+ ByteBuffer buffer = ByteBuffer.allocate(size);
|
|
|
+ // fill the buffer with data
|
|
|
+ IntBuffer intBuffer = buffer.asIntBuffer();
|
|
|
+ for(int i=0; i < size / Integer.BYTES; ++i) {
|
|
|
+ intBuffer.put(i);
|
|
|
+ }
|
|
|
+ // ensure we don't make unnecessary slices
|
|
|
+ ByteBuffer slice = VectoredReadUtils.sliceTo(buffer, 100,
|
|
|
+ createFileRange(100, size));
|
|
|
+ Assertions.assertThat(buffer)
|
|
|
+ .describedAs("Slicing on the same offset shouldn't " +
|
|
|
+ "create a new buffer")
|
|
|
+ .isEqualTo(slice);
|
|
|
+ Assertions.assertThat(slice.position())
|
|
|
+ .describedAs("Slicing should return buffers starting from position 0")
|
|
|
+ .isEqualTo(0);
|
|
|
+
|
|
|
+ // try slicing a range
|
|
|
+ final int offset = 100;
|
|
|
+ final int sliceStart = 1024;
|
|
|
+ final int sliceLength = 16 * 1024;
|
|
|
+ slice = VectoredReadUtils.sliceTo(buffer, offset,
|
|
|
+ createFileRange(offset + sliceStart, sliceLength));
|
|
|
+ // make sure they aren't the same, but use the same backing data
|
|
|
+ Assertions.assertThat(buffer)
|
|
|
+ .describedAs("Slicing on new offset should create a new buffer")
|
|
|
+ .isNotEqualTo(slice);
|
|
|
+ Assertions.assertThat(buffer.array())
|
|
|
+ .describedAs("Slicing should use the same underlying data")
|
|
|
+ .isEqualTo(slice.array());
|
|
|
+ Assertions.assertThat(slice.position())
|
|
|
+ .describedAs("Slicing should return buffers starting from position 0")
|
|
|
+ .isEqualTo(0);
|
|
|
+ // test the contents of the slice
|
|
|
+ intBuffer = slice.asIntBuffer();
|
|
|
+ for(int i=0; i < sliceLength / Integer.BYTES; ++i) {
|
|
|
+ assertEquals("i = " + i, i + sliceStart / Integer.BYTES, intBuffer.get());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Test {@link VectoredReadUtils#roundUp(long, int)}
|
|
|
+ * and {@link VectoredReadUtils#roundDown(long, int)}.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testRounding() {
|
|
|
+ for (int i = 5; i < 10; ++i) {
|
|
|
+ assertEquals("i = " + i, 5, VectoredReadUtils.roundDown(i, 5));
|
|
|
+ assertEquals("i = " + i, 10, VectoredReadUtils.roundUp(i + 1, 5));
|
|
|
+ }
|
|
|
+ assertEquals("Error while roundDown", 13, VectoredReadUtils.roundDown(13, 1));
|
|
|
+ assertEquals("Error while roundUp", 13, VectoredReadUtils.roundUp(13, 1));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Test {@link CombinedFileRange#merge(long, long, FileRange, int, int)}.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testMerge() {
|
|
|
+ // a reference to use for tracking
|
|
|
+ Object tracker1 = "one";
|
|
|
+ Object tracker2 = "two";
|
|
|
+ FileRange base = createFileRange(2000, 1000, tracker1);
|
|
|
+ CombinedFileRange mergeBase = new CombinedFileRange(2000, 3000, base);
|
|
|
+
|
|
|
+ // test when the gap between is too big
|
|
|
+ assertFalse("Large gap ranges shouldn't get merged", mergeBase.merge(5000, 6000,
|
|
|
+ createFileRange(5000, 1000), 2000, 4000));
|
|
|
+ assertUnderlyingSize(mergeBase,
|
|
|
+ "Number of ranges in merged range shouldn't increase",
|
|
|
+ 1);
|
|
|
+ assertFileRange(mergeBase, 2000, 1000);
|
|
|
+
|
|
|
+ // test when the total size gets exceeded
|
|
|
+ assertFalse("Large size ranges shouldn't get merged",
|
|
|
+ mergeBase.merge(5000, 6000,
|
|
|
+ createFileRange(5000, 1000), 2001, 3999));
|
|
|
+ assertEquals("Number of ranges in merged range shouldn't increase",
|
|
|
+ 1, mergeBase.getUnderlying().size());
|
|
|
+ assertFileRange(mergeBase, 2000, 1000);
|
|
|
+
|
|
|
+ // test when the merge works
|
|
|
+ assertTrue("ranges should get merged ", mergeBase.merge(5000, 6000,
|
|
|
+ createFileRange(5000, 1000, tracker2),
|
|
|
+ 2001, 4000));
|
|
|
+ assertUnderlyingSize(mergeBase, "merge list after merge", 2);
|
|
|
+ assertFileRange(mergeBase, 2000, 4000);
|
|
|
+
|
|
|
+ Assertions.assertThat(mergeBase.getUnderlying().get(0).getReference())
|
|
|
+ .describedAs("reference of range %s", mergeBase.getUnderlying().get(0))
|
|
|
+ .isSameAs(tracker1);
|
|
|
+ Assertions.assertThat(mergeBase.getUnderlying().get(1).getReference())
|
|
|
+ .describedAs("reference of range %s", mergeBase.getUnderlying().get(1))
|
|
|
+ .isSameAs(tracker2);
|
|
|
+
|
|
|
+ // reset the mergeBase and test with a 10:1 reduction
|
|
|
+ mergeBase = new CombinedFileRange(200, 300, base);
|
|
|
+ assertFileRange(mergeBase, 200, 100);
|
|
|
+
|
|
|
+ assertTrue("ranges should get merged ", mergeBase.merge(500, 600,
|
|
|
+ createFileRange(5000, 1000), 201, 400));
|
|
|
+ assertUnderlyingSize(mergeBase, "merge list after merge", 2);
|
|
|
+ assertFileRange(mergeBase, 200, 400);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Assert that a combined file range has a specific number of underlying ranges.
|
|
|
+ * @param combinedFileRange file range
|
|
|
+ * @param description text for errors
|
|
|
+ * @param expected expected value.
|
|
|
+ */
|
|
|
+ private static ListAssert<FileRange> assertUnderlyingSize(
|
|
|
+ final CombinedFileRange combinedFileRange,
|
|
|
+ final String description,
|
|
|
+ final int expected) {
|
|
|
+ return Assertions.assertThat(combinedFileRange.getUnderlying())
|
|
|
+ .describedAs(description)
|
|
|
+ .hasSize(expected);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Test sort and merge logic.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testSortAndMerge() {
|
|
|
+ List<FileRange> input = asList(
|
|
|
+ createFileRange(3000, 100, "1"),
|
|
|
+ createFileRange(2100, 100, null),
|
|
|
+ createFileRange(1000, 100, "3")
|
|
|
+ );
|
|
|
+ assertIsNotOrderedDisjoint(input, 100, 800);
|
|
|
+ final List<CombinedFileRange> outputList = mergeSortedRanges(
|
|
|
+ sortRanges(input), 100, 1001, 2500);
|
|
|
+
|
|
|
+ assertRangeListSize(outputList, 1);
|
|
|
+ CombinedFileRange output = outputList.get(0);
|
|
|
+ assertUnderlyingSize(output, "merged range underlying size", 3);
|
|
|
+ // range[1000,3100)
|
|
|
+ assertFileRange(output, 1000, 2100);
|
|
|
+ assertOrderedDisjoint(outputList, 100, 800);
|
|
|
+
|
|
|
+ // the minSeek doesn't allow the first two to merge
|
|
|
+ assertIsNotOrderedDisjoint(input, 100, 100);
|
|
|
+ final List<CombinedFileRange> list2 = mergeSortedRanges(
|
|
|
+ sortRanges(input),
|
|
|
+ 100, 1000, 2100);
|
|
|
+ assertRangeListSize(list2, 2);
|
|
|
+ assertRangeElement(list2, 0, 1000, 100);
|
|
|
+ assertRangeElement(list2, 1, 2100, 1000);
|
|
|
+
|
|
|
+ assertOrderedDisjoint(list2, 100, 1000);
|
|
|
+
|
|
|
+ // the maxSize doesn't allow the third range to merge
|
|
|
+ assertIsNotOrderedDisjoint(input, 100, 800);
|
|
|
+ final List<CombinedFileRange> list3 = mergeSortedRanges(
|
|
|
+ sortRanges(input),
|
|
|
+ 100, 1001, 2099);
|
|
|
+ assertRangeListSize(list3, 2);
|
|
|
+ CombinedFileRange range0 = list3.get(0);
|
|
|
+ assertFileRange(range0, 1000, 1200);
|
|
|
+ final List<FileRange> underlying = range0.getUnderlying();
|
|
|
+ assertFileRange(underlying.get(0),
|
|
|
+ 1000, 100, "3");
|
|
|
+ assertFileRange(underlying.get(1),
|
|
|
+ 2100, 100, null);
|
|
|
+ CombinedFileRange range1 = list3.get(1);
|
|
|
+ // range[3000,3100)
|
|
|
+ assertFileRange(range1, 3000, 100);
|
|
|
+ assertFileRange(range1.getUnderlying().get(0),
|
|
|
+ 3000, 100, "1");
|
|
|
+
|
|
|
+ assertOrderedDisjoint(list3, 100, 800);
|
|
|
+
|
|
|
+ // test the round up and round down (the maxSize doesn't allow any merges)
|
|
|
+ assertIsNotOrderedDisjoint(input, 16, 700);
|
|
|
+ final List<CombinedFileRange> list4 = mergeSortedRanges(
|
|
|
+ sortRanges(input),
|
|
|
+ 16, 1001, 100);
|
|
|
+ assertRangeListSize(list4, 3);
|
|
|
+ // range[992,1104)
|
|
|
+ assertRangeElement(list4, 0, 992, 112);
|
|
|
+ // range[2096,2208)
|
|
|
+ assertRangeElement(list4, 1, 2096, 112);
|
|
|
+ // range[2992,3104)
|
|
|
+ assertRangeElement(list4, 2, 2992, 112);
|
|
|
+ assertOrderedDisjoint(list4, 16, 700);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Assert that a file range has the specified start position and length.
|
|
|
+ * @param range range to validate
|
|
|
+ * @param start offset of range
|
|
|
+ * @param length range length
|
|
|
+ * @param <ELEMENT> type of range
|
|
|
+ */
|
|
|
+ private static <ELEMENT extends FileRange> void assertFileRange(
|
|
|
+ ELEMENT range, long start, int length) {
|
|
|
+
|
|
|
+ Assertions.assertThat(range)
|
|
|
+ .describedAs("file range %s", range)
|
|
|
+ .isNotNull();
|
|
|
+ Assertions.assertThat(range.getOffset())
|
|
|
+ .describedAs("offset of %s", range)
|
|
|
+ .isEqualTo(start);
|
|
|
+ Assertions.assertThat(range.getLength())
|
|
|
+ .describedAs("length of %s", range)
|
|
|
+ .isEqualTo(length);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Assert that a file range satisfies the conditions.
|
|
|
+ * @param range range to validate
|
|
|
+ * @param offset offset of range
|
|
|
+ * @param length range length
|
|
|
+ * @param reference reference; may be null.
|
|
|
+ * @param <ELEMENT> type of range
|
|
|
+ */
|
|
|
+ private static <ELEMENT extends FileRange> void assertFileRange(
|
|
|
+ ELEMENT range, long offset, int length, Object reference) {
|
|
|
+
|
|
|
+ assertFileRange(range, offset, length);
|
|
|
+ Assertions.assertThat(range.getReference())
|
|
|
+ .describedAs("reference field of file range %s", range)
|
|
|
+ .isEqualTo(reference);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Assert that a range list has a single element with the given start and length.
|
|
|
+ * @param ranges range list
|
|
|
+ * @param start start position
|
|
|
+ * @param length length of range
|
|
|
+ * @param <ELEMENT> type of range
|
|
|
+ * @return the ongoing assertion.
|
|
|
+ */
|
|
|
+ private static <ELEMENT extends FileRange> ObjectAssert<ELEMENT> assertIsSingleRange(
|
|
|
+ final List<ELEMENT> ranges,
|
|
|
+ final long start,
|
|
|
+ final int length) {
|
|
|
+ assertRangeListSize(ranges, 1);
|
|
|
+ return assertRangeElement(ranges, 0, start, length);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Assert that a range list has the exact size specified.
|
|
|
+ * @param ranges range list
|
|
|
+ * @param size expected size
|
|
|
+ * @param <ELEMENT> type of range
|
|
|
+ * @return the ongoing assertion.
|
|
|
+ */
|
|
|
+ private static <ELEMENT extends FileRange> ListAssert<ELEMENT> assertRangeListSize(
|
|
|
+ final List<ELEMENT> ranges,
|
|
|
+ final int size) {
|
|
|
+ return Assertions.assertThat(ranges)
|
|
|
+ .describedAs("coalesced ranges")
|
|
|
+ .hasSize(size);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Assert that a range list has at least the size specified.
|
|
|
+ * @param ranges range list
|
|
|
+ * @param size expected size
|
|
|
+ * @param <ELEMENT> type of range
|
|
|
+ * @return the ongoing assertion.
|
|
|
+ */
|
|
|
+ private static <ELEMENT extends FileRange> ListAssert<ELEMENT> assertRangesCountAtLeast(
|
|
|
+ final List<ELEMENT> ranges,
|
|
|
+ final int size) {
|
|
|
+ return Assertions.assertThat(ranges)
|
|
|
+ .describedAs("coalesced ranges")
|
|
|
+ .hasSizeGreaterThanOrEqualTo(size);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Assert that a range element has the given start offset and length.
|
|
|
+ * @param ranges range list
|
|
|
+ * @param index index of range
|
|
|
+ * @param start position
|
|
|
+ * @param length length of range
|
|
|
+ * @param <ELEMENT> type of range
|
|
|
+ * @return the ongoing assertion.
|
|
|
+ */
|
|
|
+ private static <ELEMENT extends FileRange> ObjectAssert<ELEMENT> assertRangeElement(
|
|
|
+ final List<ELEMENT> ranges,
|
|
|
+ final int index,
|
|
|
+ final long start,
|
|
|
+ final int length) {
|
|
|
+ return assertRangesCountAtLeast(ranges, index + 1)
|
|
|
+ .element(index)
|
|
|
+ .describedAs("range")
|
|
|
+ .satisfies(r -> assertFileRange(r, start, length));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Assert that a file range is ordered and disjoint.
|
|
|
+ * @param input the list of input ranges.
|
|
|
+ * @param chunkSize the size of the chunks that the offset and end must align to.
|
|
|
+ * @param minimumSeek the minimum distance between ranges.
|
|
|
+ */
|
|
|
+ private static void assertOrderedDisjoint(
|
|
|
+ List<? extends FileRange> input,
|
|
|
+ int chunkSize,
|
|
|
+ int minimumSeek) {
|
|
|
+ Assertions.assertThat(isOrderedDisjoint(input, chunkSize, minimumSeek))
|
|
|
+ .describedAs("ranges are ordered and disjoint")
|
|
|
+ .isTrue();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Assert that a file range is not ordered or not disjoint.
|
|
|
+ * @param input the list of input ranges.
|
|
|
+ * @param chunkSize the size of the chunks that the offset and end must align to.
|
|
|
+ * @param minimumSeek the minimum distance between ranges.
|
|
|
+ */
|
|
|
+ private static <ELEMENT extends FileRange> void assertIsNotOrderedDisjoint(
|
|
|
+ List<ELEMENT> input,
|
|
|
+ int chunkSize,
|
|
|
+ int minimumSeek) {
|
|
|
+ Assertions.assertThat(isOrderedDisjoint(input, chunkSize, minimumSeek))
|
|
|
+ .describedAs("Ranges are non disjoint/ordered")
|
|
|
+ .isFalse();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Test sort and merge.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testSortAndMergeMoreCases() throws Exception {
|
|
|
+ List<FileRange> input = asList(
|
|
|
+ createFileRange(3000, 110),
|
|
|
+ createFileRange(3000, 100),
|
|
|
+ createFileRange(2100, 100),
|
|
|
+ createFileRange(1000, 100)
|
|
|
+ );
|
|
|
+ assertIsNotOrderedDisjoint(input, 100, 800);
|
|
|
+ List<CombinedFileRange> outputList = mergeSortedRanges(
|
|
|
+ sortRanges(input), 1, 1001, 2500);
|
|
|
+ Assertions.assertThat(outputList)
|
|
|
+ .describedAs("merged range size")
|
|
|
+ .hasSize(1);
|
|
|
+ CombinedFileRange output = outputList.get(0);
|
|
|
+ assertUnderlyingSize(output, "merged range underlying size", 4);
|
|
|
+
|
|
|
+ assertFileRange(output, 1000, 2110);
|
|
|
+
|
|
|
+ assertOrderedDisjoint(outputList, 1, 800);
|
|
|
+
|
|
|
+ outputList = mergeSortedRanges(
|
|
|
+ sortRanges(input), 100, 1001, 2500);
|
|
|
+ assertRangeListSize(outputList, 1);
|
|
|
+
|
|
|
+ output = outputList.get(0);
|
|
|
+ assertUnderlyingSize(output, "merged range underlying size", 4);
|
|
|
+ assertFileRange(output, 1000, 2200);
|
|
|
+
|
|
|
+ assertOrderedDisjoint(outputList, 1, 800);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void testRejectOverlappingRanges() throws Exception {
|
|
|
+ List<FileRange> input = asList(
|
|
|
+ createFileRange(100, 100),
|
|
|
+ createFileRange(200, 100),
|
|
|
+ createFileRange(250, 100)
|
|
|
+ );
|
|
|
+
|
|
|
+ intercept(IllegalArgumentException.class,
|
|
|
+ () -> validateAndSortRanges(input, Optional.empty()));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Special case of overlap: the ranges are equal.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testDuplicateRangesRaisesIllegalArgument() throws Exception {
|
|
|
+
|
|
|
+ List<FileRange> input1 = asList(
|
|
|
+ createFileRange(100, 100),
|
|
|
+ createFileRange(500, 100),
|
|
|
+ createFileRange(1000, 100),
|
|
|
+ createFileRange(1000, 100)
|
|
|
+ );
|
|
|
+
|
|
|
+ intercept(IllegalArgumentException.class,
|
|
|
+ () -> validateAndSortRanges(input1, Optional.empty()));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Consecutive ranges MUST pass.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testConsecutiveRangesAreValid() throws Throwable {
|
|
|
+
|
|
|
+ validateAndSortRanges(
|
|
|
+ asList(
|
|
|
+ createFileRange(100, 100),
|
|
|
+ createFileRange(200, 100),
|
|
|
+ createFileRange(300, 100)),
|
|
|
+ Optional.empty());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * If the maximum zie for merging is zero, ranges do not get merged.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testMaxSizeZeroDisablesMerging() {
|
|
|
+ List<FileRange> randomRanges = asList(
|
|
|
+ createFileRange(3000, 110),
|
|
|
+ createFileRange(3000, 100),
|
|
|
+ createFileRange(2100, 100)
|
|
|
+ );
|
|
|
+ assertEqualRangeCountsAfterMerging(randomRanges, 1, 1, 0);
|
|
|
+ assertEqualRangeCountsAfterMerging(randomRanges, 1, 0, 0);
|
|
|
+ assertEqualRangeCountsAfterMerging(randomRanges, 1, 100, 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Assert that the range count is the same after merging.
|
|
|
+ * @param inputRanges input ranges
|
|
|
+ * @param chunkSize chunk size for merge
|
|
|
+ * @param minimumSeek minimum seek for merge
|
|
|
+ * @param maxSize max size for merge
|
|
|
+ */
|
|
|
+ private static void assertEqualRangeCountsAfterMerging(List<FileRange> inputRanges,
|
|
|
+ int chunkSize,
|
|
|
+ int minimumSeek,
|
|
|
+ int maxSize) {
|
|
|
+ List<CombinedFileRange> combinedFileRanges = mergeSortedRanges(
|
|
|
+ inputRanges, chunkSize, minimumSeek, maxSize);
|
|
|
+ assertRangeListSize(combinedFileRanges, inputRanges.size());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Stream to read from.
|
|
|
+ */
|
|
|
+ interface Stream extends PositionedReadable, ByteBufferPositionedReadable {
|
|
|
+ // nothing
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Fill a buffer with bytes incremented from 0.
|
|
|
+ * @param buffer target buffer.
|
|
|
+ */
|
|
|
+ private static void fillBuffer(ByteBuffer buffer) {
|
|
|
+ byte b = 0;
|
|
|
+ while (buffer.remaining() > 0) {
|
|
|
+ buffer.put(b++);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Read a single range, verify the future completed and validate the buffer
|
|
|
+ * returned.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testReadSingleRange() throws Exception {
|
|
|
+ final Stream stream = mockStreamWithReadFully();
|
|
|
+ CompletableFuture<ByteBuffer> result =
|
|
|
+ readRangeFrom(stream, createFileRange(1000, 100),
|
|
|
+ ByteBuffer::allocate);
|
|
|
+ assertFutureCompletedSuccessfully(result);
|
|
|
+ ByteBuffer buffer = result.get();
|
|
|
+ assertEquals("Size of result buffer", 100, buffer.remaining());
|
|
|
+ byte b = 0;
|
|
|
+ while (buffer.remaining() > 0) {
|
|
|
+ assertEquals("remain = " + buffer.remaining(), b++, buffer.get());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Read a single range with IOE fault injection; verify the failure
|
|
|
+ * is reported.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testReadWithIOE() throws Exception {
|
|
|
+ final Stream stream = mockStreamWithReadFully();
|
|
|
+
|
|
|
+ Mockito.doThrow(new IOException("foo"))
|
|
|
+ .when(stream).readFully(ArgumentMatchers.anyLong(),
|
|
|
+ ArgumentMatchers.any(ByteBuffer.class));
|
|
|
+ CompletableFuture<ByteBuffer> result =
|
|
|
+ readRangeFrom(stream, createFileRange(1000, 100), ByteBuffer::allocate);
|
|
|
+ assertFutureFailedExceptionally(result);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Read a range, first successfully, then with an IOE.
|
|
|
+ * the output of the first read is validated.
|
|
|
+ * @param allocate allocator to use
|
|
|
+ */
|
|
|
+ private static void runReadRangeFromPositionedReadable(IntFunction<ByteBuffer> allocate)
|
|
|
+ throws Exception {
|
|
|
+ PositionedReadable stream = Mockito.mock(PositionedReadable.class);
|
|
|
+ Mockito.doAnswer(invocation -> {
|
|
|
+ byte b=0;
|
|
|
+ byte[] buffer = invocation.getArgument(1);
|
|
|
+ for(int i=0; i < buffer.length; ++i) {
|
|
|
+ buffer[i] = b++;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }).when(stream).readFully(ArgumentMatchers.anyLong(),
|
|
|
+ ArgumentMatchers.any(), ArgumentMatchers.anyInt(),
|
|
|
+ ArgumentMatchers.anyInt());
|
|
|
+ CompletableFuture<ByteBuffer> result =
|
|
|
+ readRangeFrom(stream, createFileRange(1000, 100),
|
|
|
+ allocate);
|
|
|
+ assertFutureCompletedSuccessfully(result);
|
|
|
+ ByteBuffer buffer = result.get();
|
|
|
+ assertEquals("Size of result buffer", 100, buffer.remaining());
|
|
|
+ validateBuffer("buffer", buffer, 0);
|
|
|
+
|
|
|
+
|
|
|
+ // test an IOException
|
|
|
+ Mockito.reset(stream);
|
|
|
+ Mockito.doThrow(new IOException("foo"))
|
|
|
+ .when(stream).readFully(ArgumentMatchers.anyLong(),
|
|
|
+ ArgumentMatchers.any(), ArgumentMatchers.anyInt(),
|
|
|
+ ArgumentMatchers.anyInt());
|
|
|
+ result = readRangeFrom(stream, createFileRange(1000, 100),
|
|
|
+ ByteBuffer::allocate);
|
|
|
+ assertFutureFailedExceptionally(result);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Read into an on heap buffer.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testReadRangeArray() throws Exception {
|
|
|
+ runReadRangeFromPositionedReadable(ByteBuffer::allocate);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Read into an off-heap buffer.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testReadRangeDirect() throws Exception {
|
|
|
+ runReadRangeFromPositionedReadable(ByteBuffer::allocateDirect);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Validate a buffer where the first byte value is {@code start}
|
|
|
+ * and the subsequent bytes are from that value incremented by one, wrapping
|
|
|
+ * at 256.
|
|
|
+ * @param message error message.
|
|
|
+ * @param buffer buffer
|
|
|
+ * @param start first byte of the buffer.
|
|
|
+ */
|
|
|
+ private static void validateBuffer(String message, ByteBuffer buffer, int start) {
|
|
|
+ byte expected = (byte) start;
|
|
|
+ while (buffer.remaining() > 0) {
|
|
|
+ assertEquals(message + " remain: " + buffer.remaining(), expected,
|
|
|
+ buffer.get());
|
|
|
+ // increment with wrapping.
|
|
|
+ expected = (byte) (expected + 1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Validate basic read vectored works as expected.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testReadVectored() throws Exception {
|
|
|
+ List<FileRange> input = asList(createFileRange(0, 100),
|
|
|
+ createFileRange(100_000, 100, "this"),
|
|
|
+ createFileRange(200_000, 100, "that"));
|
|
|
+ runAndValidateVectoredRead(input);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Verify a read with length 0 completes with a buffer of size 0.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testReadVectoredZeroBytes() throws Exception {
|
|
|
+ List<FileRange> input = asList(createFileRange(0, 0, "1"),
|
|
|
+ createFileRange(100_000, 100, "2"),
|
|
|
+ createFileRange(200_000, 0, "3"));
|
|
|
+ runAndValidateVectoredRead(input);
|
|
|
+ // look up by name and validate.
|
|
|
+ final FileRange r1 = retrieve(input, "1");
|
|
|
+ Assertions.assertThat(r1.getData().get().limit())
|
|
|
+ .describedAs("Data limit of %s", r1)
|
|
|
+ .isEqualTo(0);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Retrieve a range from a list of ranges by its (string) reference.
|
|
|
+ * @param input input list
|
|
|
+ * @param key key to look up
|
|
|
+ * @return the range
|
|
|
+ * @throws IllegalArgumentException if the range is not found.
|
|
|
+ */
|
|
|
+ private static FileRange retrieve(List<FileRange> input, String key) {
|
|
|
+ return input.stream()
|
|
|
+ .filter(r -> key.equals(r.getReference()))
|
|
|
+ .findFirst()
|
|
|
+ .orElseThrow(() -> new IllegalArgumentException("No range with key " + key));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Mock run a vectored read and validate the results with the assertions.
|
|
|
+ * <ol>
|
|
|
+ * <li> {@code ByteBufferPositionedReadable.readFully()} is invoked once per range.</li>
|
|
|
+ * <li> The buffers are filled with data</li>
|
|
|
+ * </ol>
|
|
|
+ * @param input input ranges
|
|
|
+ * @throws Exception failure
|
|
|
+ */
|
|
|
+ private void runAndValidateVectoredRead(List<FileRange> input)
|
|
|
+ throws Exception {
|
|
|
+ final Stream stream = mockStreamWithReadFully();
|
|
|
+ // should not merge the ranges
|
|
|
+ readVectored(stream, input, ByteBuffer::allocate);
|
|
|
+ // readFully is invoked once per range
|
|
|
+ Mockito.verify(stream, Mockito.times(input.size()))
|
|
|
+ .readFully(ArgumentMatchers.anyLong(), ArgumentMatchers.any(ByteBuffer.class));
|
|
|
+
|
|
|
+ // validate each buffer
|
|
|
+ for (int b = 0; b < input.size(); ++b) {
|
|
|
+ validateBuffer("buffer " + b, input.get(b).getData().get(), 0);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Mock a stream with {@link Stream#readFully(long, ByteBuffer)}.
|
|
|
+ * Filling in each byte buffer.
|
|
|
+ * @return the stream
|
|
|
+ * @throws IOException (side effect of the mocking;
|
|
|
+ */
|
|
|
+ private static Stream mockStreamWithReadFully() throws IOException {
|
|
|
+ Stream stream = Mockito.mock(Stream.class);
|
|
|
+ Mockito.doAnswer(invocation -> {
|
|
|
+ fillBuffer(invocation.getArgument(1));
|
|
|
+ return null;
|
|
|
+ }).when(stream).readFully(ArgumentMatchers.anyLong(),
|
|
|
+ ArgumentMatchers.any(ByteBuffer.class));
|
|
|
+ return stream;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Empty ranges cannot be sorted.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testEmptyRangesRaisesIllegalArgument() throws Throwable {
|
|
|
+ intercept(IllegalArgumentException.class,
|
|
|
+ () -> validateAndSortRanges(Collections.emptyList(), Optional.empty()));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Reject negative offsets.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testNegativeOffsetRaisesEOF() throws Throwable {
|
|
|
+ intercept(EOFException.class, () ->
|
|
|
+ validateAndSortRanges(asList(
|
|
|
+ createFileRange(1000, 100),
|
|
|
+ createFileRange(-1000, 100)),
|
|
|
+ Optional.empty()));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Reject negative lengths.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testNegativePositionRaisesIllegalArgument() throws Throwable {
|
|
|
+ intercept(IllegalArgumentException.class, () ->
|
|
|
+ validateAndSortRanges(asList(
|
|
|
+ createFileRange(1000, 100),
|
|
|
+ createFileRange(1000, -100)),
|
|
|
+ Optional.empty()));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * A read for a whole file is valid.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testReadWholeFile() throws Exception {
|
|
|
+ final int length = 1000;
|
|
|
+
|
|
|
+ // Read whole file as one element
|
|
|
+ final List<? extends FileRange> ranges = validateAndSortRanges(
|
|
|
+ asList(createFileRange(0, length)),
|
|
|
+ Optional.of((long) length));
|
|
|
+
|
|
|
+ assertIsSingleRange(ranges, 0, length);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * A read from start of file to past EOF is rejected.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testReadPastEOFRejected() throws Exception {
|
|
|
+ final int length = 1000;
|
|
|
+ intercept(EOFException.class, () ->
|
|
|
+ validateAndSortRanges(
|
|
|
+ asList(createFileRange(0, length + 1)),
|
|
|
+ Optional.of((long) length)));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * If the start offset is at the end of the file: an EOFException.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testReadStartingPastEOFRejected() throws Exception {
|
|
|
+ final int length = 1000;
|
|
|
+ intercept(EOFException.class, () ->
|
|
|
+ validateAndSortRanges(
|
|
|
+ asList(createFileRange(length, 0)),
|
|
|
+ Optional.of((long) length)));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * A read from just below the EOF to the end of the file is valid.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testReadUpToEOF() throws Exception {
|
|
|
+ final int length = 1000;
|
|
|
+
|
|
|
+ final int p = length - 1;
|
|
|
+ assertIsSingleRange(
|
|
|
+ validateAndSortRanges(
|
|
|
+ asList(createFileRange(p, 1)),
|
|
|
+ Optional.of((long) length)),
|
|
|
+ p, 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * A read from just below the EOF to the just past the end of the file is rejected
|
|
|
+ * with EOFException.
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ public void testReadOverEOFRejected() throws Exception {
|
|
|
+ final long length = 1000;
|
|
|
+
|
|
|
+ intercept(EOFException.class, () ->
|
|
|
+ validateAndSortRanges(
|
|
|
+ asList(createFileRange(length - 1, 2)),
|
|
|
+ Optional.of(length)));
|
|
|
+ }
|
|
|
+}
|