/* * * 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.cassandra.io.util; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; import java.nio.charset.Charset; import java.util.Arrays; import java.util.Random; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static org.junit.Assert.*; import org.apache.cassandra.io.compress.BufferType; import org.apache.cassandra.utils.ByteBufferUtil; public class RandomAccessReaderTest { private static final Logger logger = LoggerFactory.getLogger(RandomAccessReaderTest.class); private static final class Parameters { public final long fileLength; public final int bufferSize; public BufferType bufferType; public int maxSegmentSize; public boolean mmappedRegions; public byte[] expected; Parameters(long fileLength, int bufferSize) { this.fileLength = fileLength; this.bufferSize = bufferSize; this.bufferType = BufferType.OFF_HEAP; this.maxSegmentSize = MmappedRegions.MAX_SEGMENT_SIZE; this.mmappedRegions = false; this.expected = "The quick brown fox jumps over the lazy dog".getBytes(FileUtils.CHARSET); } public Parameters mmappedRegions(boolean mmappedRegions) { this.mmappedRegions = mmappedRegions; return this; } public Parameters bufferType(BufferType bufferType) { this.bufferType = bufferType; return this; } public Parameters maxSegmentSize(int maxSegmentSize) { this.maxSegmentSize = maxSegmentSize; return this; } public Parameters expected(byte[] expected) { this.expected = expected; return this; } } @Test public void testBufferedOffHeap() throws IOException { testReadFully(new Parameters(8192, 4096).bufferType(BufferType.OFF_HEAP)); } @Test public void testBufferedOnHeap() throws IOException { testReadFully(new Parameters(8192, 4096).bufferType(BufferType.ON_HEAP)); } @Test public void testBigBufferSize() throws IOException { testReadFully(new Parameters(8192, 65536).bufferType(BufferType.ON_HEAP)); } @Test public void testTinyBufferSize() throws IOException { testReadFully(new Parameters(8192, 16).bufferType(BufferType.ON_HEAP)); } @Test public void testOneSegment() throws IOException { testReadFully(new Parameters(8192, 4096).mmappedRegions(true)); } @Test public void testMultipleSegments() throws IOException { testReadFully(new Parameters(8192, 4096).mmappedRegions(true).maxSegmentSize(1024)); } @Test public void testVeryLarge() throws IOException { final long SIZE = 1L << 32; // 2GB Parameters params = new Parameters(SIZE, 1 << 20); // 1MB try(ChannelProxy channel = new ChannelProxy("abc", new FakeFileChannel(SIZE))) { RandomAccessReader.Builder builder = new RandomAccessReader.Builder(channel) .bufferType(params.bufferType) .bufferSize(params.bufferSize); try(RandomAccessReader reader = builder.build()) { assertEquals(channel.size(), reader.length()); assertEquals(channel.size(), reader.bytesRemaining()); assertEquals(Integer.MAX_VALUE, reader.available()); assertEquals(channel.size(), reader.skip(channel.size())); assertTrue(reader.isEOF()); assertEquals(0, reader.bytesRemaining()); } } } /** A fake file channel that simply increments the position and doesn't * actually read anything. We use it to simulate very large files, > 2G. */ private static final class FakeFileChannel extends FileChannel { private final long size; private long position; FakeFileChannel(long size) { this.size = size; } public int read(ByteBuffer dst) { int ret = dst.remaining(); position += ret; dst.position(dst.limit()); return ret; } public long read(ByteBuffer[] dsts, int offset, int length) { throw new UnsupportedOperationException(); } public int write(ByteBuffer src) { throw new UnsupportedOperationException(); } public long write(ByteBuffer[] srcs, int offset, int length) { throw new UnsupportedOperationException(); } public long position() { return position; } public FileChannel position(long newPosition) { position = newPosition; return this; } public long size() { return size; } public FileChannel truncate(long size) { throw new UnsupportedOperationException(); } public void force(boolean metaData) { throw new UnsupportedOperationException(); } public long transferTo(long position, long count, WritableByteChannel target) { throw new UnsupportedOperationException(); } public long transferFrom(ReadableByteChannel src, long position, long count) { throw new UnsupportedOperationException(); } public int read(ByteBuffer dst, long position) { int ret = dst.remaining(); this.position = position + ret; dst.position(dst.limit()); return ret; } public int write(ByteBuffer src, long position) { throw new UnsupportedOperationException(); } public MappedByteBuffer map(MapMode mode, long position, long size) { throw new UnsupportedOperationException(); } public FileLock lock(long position, long size, boolean shared) { throw new UnsupportedOperationException(); } public FileLock tryLock(long position, long size, boolean shared) { throw new UnsupportedOperationException(); } protected void implCloseChannel() { } } private static File writeFile(Parameters params) throws IOException { final File f = File.createTempFile("testReadFully", "1"); f.deleteOnExit(); try(SequentialWriter writer = SequentialWriter.open(f)) { long numWritten = 0; while (numWritten < params.fileLength) { writer.write(params.expected); numWritten += params.expected.length; } writer.finish(); } assert f.exists(); assert f.length() >= params.fileLength; return f; } private static void testReadFully(Parameters params) throws IOException { final File f = writeFile(params); try(ChannelProxy channel = new ChannelProxy(f)) { RandomAccessReader.Builder builder = new RandomAccessReader.Builder(channel) .bufferType(params.bufferType) .bufferSize(params.bufferSize); if (params.mmappedRegions) builder.regions(MmappedRegions.map(channel, f.length())); try(RandomAccessReader reader = builder.build()) { assertEquals(f.getAbsolutePath(), reader.getPath()); assertEquals(f.length(), reader.length()); assertEquals(f.length(), reader.bytesRemaining()); assertEquals(Math.min(Integer.MAX_VALUE, f.length()), reader.available()); byte[] b = new byte[params.expected.length]; long numRead = 0; while (numRead < params.fileLength) { reader.readFully(b); assertTrue(Arrays.equals(params.expected, b)); numRead += b.length; } assertTrue(reader.isEOF()); assertEquals(0, reader.bytesRemaining()); } if (builder.regions != null) assertNull(builder.regions.close(null)); } } @Test public void testReadBytes() throws IOException { File f = File.createTempFile("testReadBytes", "1"); final String expected = "The quick brown fox jumps over the lazy dog"; try(SequentialWriter writer = SequentialWriter.open(f)) { writer.write(expected.getBytes()); writer.finish(); } assert f.exists(); try(ChannelProxy channel = new ChannelProxy(f); RandomAccessReader reader = new RandomAccessReader.Builder(channel).build()) { assertEquals(f.getAbsolutePath(), reader.getPath()); assertEquals(expected.length(), reader.length()); ByteBuffer b = ByteBufferUtil.read(reader, expected.length()); assertEquals(expected, new String(b.array(), Charset.forName("UTF-8"))); assertTrue(reader.isEOF()); assertEquals(0, reader.bytesRemaining()); } } @Test public void testReset() throws IOException { File f = File.createTempFile("testMark", "1"); final String expected = "The quick brown fox jumps over the lazy dog"; final int numIterations = 10; try(SequentialWriter writer = SequentialWriter.open(f)) { for (int i = 0; i < numIterations; i++) writer.write(expected.getBytes()); writer.finish(); } assert f.exists(); try(ChannelProxy channel = new ChannelProxy(f); RandomAccessReader reader = new RandomAccessReader.Builder(channel).build()) { assertEquals(expected.length() * numIterations, reader.length()); ByteBuffer b = ByteBufferUtil.read(reader, expected.length()); assertEquals(expected, new String(b.array(), Charset.forName("UTF-8"))); assertFalse(reader.isEOF()); assertEquals((numIterations - 1) * expected.length(), reader.bytesRemaining()); DataPosition mark = reader.mark(); assertEquals(0, reader.bytesPastMark()); assertEquals(0, reader.bytesPastMark(mark)); for (int i = 0; i < (numIterations - 1); i++) { b = ByteBufferUtil.read(reader, expected.length()); assertEquals(expected, new String(b.array(), Charset.forName("UTF-8"))); } assertTrue(reader.isEOF()); assertEquals(expected.length() * (numIterations - 1), reader.bytesPastMark()); assertEquals(expected.length() * (numIterations - 1), reader.bytesPastMark(mark)); reader.reset(mark); assertEquals(0, reader.bytesPastMark()); assertEquals(0, reader.bytesPastMark(mark)); assertFalse(reader.isEOF()); for (int i = 0; i < (numIterations - 1); i++) { b = ByteBufferUtil.read(reader, expected.length()); assertEquals(expected, new String(b.array(), Charset.forName("UTF-8"))); } reader.reset(); assertEquals(0, reader.bytesPastMark()); assertEquals(0, reader.bytesPastMark(mark)); assertFalse(reader.isEOF()); for (int i = 0; i < (numIterations - 1); i++) { b = ByteBufferUtil.read(reader, expected.length()); assertEquals(expected, new String(b.array(), Charset.forName("UTF-8"))); } assertTrue(reader.isEOF()); } } @Test public void testSeekSingleThread() throws IOException, InterruptedException { testSeek(1); } @Test public void testSeekMultipleThreads() throws IOException, InterruptedException { testSeek(10); } private static void testSeek(int numThreads) throws IOException, InterruptedException { final File f = File.createTempFile("testMark", "1"); final byte[] expected = new byte[1 << 16]; long seed = System.nanoTime(); //seed = 365238103404423L; logger.info("Seed {}", seed); Random r = new Random(seed); r.nextBytes(expected); try(SequentialWriter writer = SequentialWriter.open(f)) { writer.write(expected); writer.finish(); } assert f.exists(); try(final ChannelProxy channel = new ChannelProxy(f)) { final Runnable worker = () -> { try(RandomAccessReader reader = new RandomAccessReader.Builder(channel).build()) { assertEquals(expected.length, reader.length()); ByteBuffer b = ByteBufferUtil.read(reader, expected.length); assertTrue(Arrays.equals(expected, b.array())); assertTrue(reader.isEOF()); assertEquals(0, reader.bytesRemaining()); reader.seek(0); b = ByteBufferUtil.read(reader, expected.length); assertTrue(Arrays.equals(expected, b.array())); assertTrue(reader.isEOF()); assertEquals(0, reader.bytesRemaining()); for (int i = 0; i < 10; i++) { int pos = r.nextInt(expected.length); reader.seek(pos); assertEquals(pos, reader.getPosition()); ByteBuffer buf = ByteBuffer.wrap(expected, pos, expected.length - pos) .order(ByteOrder.BIG_ENDIAN); while (reader.bytesRemaining() > 4) assertEquals(buf.getInt(), reader.readInt()); } reader.close(); } catch (Exception ex) { ex.printStackTrace(); fail(ex.getMessage()); } }; if (numThreads == 1) { worker.run(); } else { ExecutorService executor = Executors.newFixedThreadPool(numThreads); for (int i = 0; i < numThreads; i++) executor.submit(worker); executor.shutdown(); executor.awaitTermination(1, TimeUnit.MINUTES); } } } }