/*
* The Alluxio Open Foundation licenses this work under the Apache License, version 2.0
* (the "License"). You may not use this work except in compliance with the License, which is
* available at www.apache.org/licenses/LICENSE-2.0
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied, as more fully set forth in the License.
*
* See the NOTICE file distributed with this work for information regarding copyright ownership.
*/
package alluxio.client.file;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyByte;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import alluxio.AlluxioURI;
import alluxio.ConfigurationTestUtils;
import alluxio.Constants;
import alluxio.LoginUserRule;
import alluxio.client.UnderStorageType;
import alluxio.client.WriteType;
import alluxio.client.block.AlluxioBlockStore;
import alluxio.client.block.BlockWorkerInfo;
import alluxio.client.block.stream.BlockOutStream;
import alluxio.client.block.stream.TestBlockOutStream;
import alluxio.client.block.stream.TestUnderFileSystemFileOutStream;
import alluxio.client.block.stream.UnderFileSystemFileOutStream;
import alluxio.client.file.options.CompleteFileOptions;
import alluxio.client.file.options.GetStatusOptions;
import alluxio.client.file.options.OutStreamOptions;
import alluxio.client.util.ClientTestUtils;
import alluxio.exception.ExceptionMessage;
import alluxio.exception.PreconditionMessage;
import alluxio.resource.DummyCloseableResource;
import alluxio.security.GroupMappingServiceTestUtils;
import alluxio.util.io.BufferUtils;
import alluxio.wire.FileInfo;
import alluxio.wire.WorkerNetAddress;
import com.google.common.collect.Lists;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Tests for the {@link FileOutStream} class.
*/
@RunWith(PowerMockRunner.class)
@PrepareForTest({FileSystemContext.class, FileSystemMasterClient.class, AlluxioBlockStore.class,
UnderFileSystemFileOutStream.class})
public class FileOutStreamTest {
@Rule
public LoginUserRule mLoginUser = new LoginUserRule("Test");
private static final long BLOCK_LENGTH = 100L;
private static final AlluxioURI FILE_NAME = new AlluxioURI("/file");
private FileSystemContext mFileSystemContext;
private AlluxioBlockStore mBlockStore;
private FileSystemMasterClient mFileSystemMasterClient;
private Map<Long, TestBlockOutStream> mAlluxioOutStreamMap;
private TestUnderFileSystemFileOutStream mUnderStorageOutputStream;
private AtomicBoolean mUnderStorageFlushed;
private FileOutStream mTestStream;
/**
* Sets up the different contexts and clients before a test runs.
*/
@Before
public void before() throws Exception {
GroupMappingServiceTestUtils.resetCache();
ClientTestUtils.setSmallBufferSizes();
// PowerMock enums and final classes
mFileSystemContext = PowerMockito.mock(FileSystemContext.class);
mBlockStore = PowerMockito.mock(AlluxioBlockStore.class);
mFileSystemMasterClient = PowerMockito.mock(FileSystemMasterClient.class);
PowerMockito.mockStatic(AlluxioBlockStore.class);
PowerMockito.when(AlluxioBlockStore.create(mFileSystemContext)).thenReturn(mBlockStore);
when(mFileSystemContext.acquireMasterClientResource())
.thenReturn(new DummyCloseableResource<>(mFileSystemMasterClient));
when(mFileSystemMasterClient.getStatus(any(AlluxioURI.class), any(GetStatusOptions.class)))
.thenReturn(new URIStatus(new FileInfo()));
// Return sequentially increasing numbers for new block ids
when(mFileSystemMasterClient.getNewBlockIdForFile(FILE_NAME))
.thenAnswer(new Answer<Long>() {
private long mCount = 0;
@Override
public Long answer(InvocationOnMock invocation) throws Throwable {
return mCount++;
}
});
// Set up out streams. When they are created, add them to outStreamMap
final Map<Long, TestBlockOutStream> outStreamMap = new HashMap<>();
when(mBlockStore.getOutStream(anyLong(), eq(BLOCK_LENGTH),
any(OutStreamOptions.class))).thenAnswer(new Answer<TestBlockOutStream>() {
@Override
public TestBlockOutStream answer(InvocationOnMock invocation) throws Throwable {
Long blockId = invocation.getArgumentAt(0, Long.class);
if (!outStreamMap.containsKey(blockId)) {
TestBlockOutStream newStream =
new TestBlockOutStream(ByteBuffer.allocate(1000), BLOCK_LENGTH);
outStreamMap.put(blockId, newStream);
}
return outStreamMap.get(blockId);
}
});
BlockWorkerInfo workerInfo =
new BlockWorkerInfo(new WorkerNetAddress().setHost("localhost").setRpcPort(1)
.setDataPort(2).setWebPort(3), Constants.GB, 0);
when(mBlockStore.getWorkerInfoList()).thenReturn(Lists.newArrayList(workerInfo));
mAlluxioOutStreamMap = outStreamMap;
// Create an under storage stream so that we can check whether it has been flushed
final AtomicBoolean underStorageFlushed = new AtomicBoolean(false);
mUnderStorageOutputStream =
new TestUnderFileSystemFileOutStream(ByteBuffer.allocate(5000)) {
@Override
public void flush() throws IOException {
super.flush();
underStorageFlushed.set(true);
}
};
mUnderStorageFlushed = underStorageFlushed;
PowerMockito.mockStatic(UnderFileSystemFileOutStream.class);
PowerMockito.when(
UnderFileSystemFileOutStream.create(any(FileSystemContext.class),
any(WorkerNetAddress.class), any(OutStreamOptions.class))).thenReturn(
mUnderStorageOutputStream);
OutStreamOptions options = OutStreamOptions.defaults().setBlockSizeBytes(BLOCK_LENGTH)
.setWriteType(WriteType.CACHE_THROUGH).setUfsPath(FILE_NAME.getPath());
mTestStream = createTestStream(FILE_NAME, options);
}
@After
public void after() {
ConfigurationTestUtils.resetConfiguration();
ClientTestUtils.resetClient();
}
/**
* Tests that a single byte is written to the out stream correctly.
*/
@Test
public void singleByteWrite() throws Exception {
mTestStream.write(5);
mTestStream.close();
Assert.assertArrayEquals(new byte[] {5}, mAlluxioOutStreamMap.get(0L).getWrittenData());
}
/**
* Tests that many bytes, written one at a time, are written to the out streams correctly.
*/
@Test
public void manyBytesWrite() throws IOException {
int bytesToWrite = (int) ((BLOCK_LENGTH * 5) + (BLOCK_LENGTH / 2));
for (int i = 0; i < bytesToWrite; i++) {
mTestStream.write(i);
}
mTestStream.close();
verifyIncreasingBytesWritten(bytesToWrite);
}
/**
* Tests that writing a buffer all at once will write bytes to the out streams correctly.
*/
@Test
public void writeBuffer() throws IOException {
int bytesToWrite = (int) ((BLOCK_LENGTH * 5) + (BLOCK_LENGTH / 2));
mTestStream.write(BufferUtils.getIncreasingByteArray(bytesToWrite));
mTestStream.close();
verifyIncreasingBytesWritten(bytesToWrite);
}
/**
* Tests writing a buffer at an offset.
*/
@Test
public void writeOffset() throws IOException {
int bytesToWrite = (int) ((BLOCK_LENGTH * 5) + (BLOCK_LENGTH / 2));
int offset = (int) (BLOCK_LENGTH / 3);
mTestStream.write(BufferUtils.getIncreasingByteArray(bytesToWrite + offset), offset,
bytesToWrite);
mTestStream.close();
verifyIncreasingBytesWritten(offset, bytesToWrite);
}
/**
* Tests that {@link FileOutStream#close()} will close but not cancel the underlying out streams.
* Also checks that {@link FileOutStream#close()} persists and completes the file.
*/
@Test
public void close() throws Exception {
mTestStream.write(BufferUtils.getIncreasingByteArray((int) (BLOCK_LENGTH * 1.5)));
mTestStream.close();
for (long streamIndex = 0; streamIndex < 2; streamIndex++) {
Assert.assertFalse(mAlluxioOutStreamMap.get(streamIndex).isCanceled());
Assert.assertTrue(mAlluxioOutStreamMap.get(streamIndex).isClosed());
}
verify(mFileSystemMasterClient).completeFile(eq(FILE_NAME), any(CompleteFileOptions.class));
}
/**
* Tests that {@link FileOutStream#cancel()} will cancel and close the underlying out streams, and
* delete from the under file system when the delegation flag is set. Also makes sure that
* cancel() doesn't persist or complete the file.
*/
@Test
public void cancelWithDelegation() throws Exception {
mTestStream.write(BufferUtils.getIncreasingByteArray((int) (BLOCK_LENGTH * 1.5)));
mTestStream.cancel();
for (long streamIndex = 0; streamIndex < 2; streamIndex++) {
Assert.assertTrue(mAlluxioOutStreamMap.get(streamIndex).isClosed());
Assert.assertTrue(mAlluxioOutStreamMap.get(streamIndex).isCanceled());
}
// Don't complete the file if the stream was canceled
verify(mFileSystemMasterClient, times(0)).completeFile(any(AlluxioURI.class),
any(CompleteFileOptions.class));
}
/**
* Tests that {@link FileOutStream#flush()} will flush the under store stream.
*/
@Test
public void flush() throws IOException {
Assert.assertFalse(mUnderStorageFlushed.get());
mTestStream.flush();
Assert.assertTrue(mUnderStorageFlushed.get());
}
/**
* Tests that if an exception is thrown by the underlying out stream, and the user is using
* {@link UnderStorageType#NO_PERSIST} for their under storage type, the correct exception
* message will be thrown.
*/
@Test
public void cacheWriteExceptionNonSyncPersist() throws IOException {
OutStreamOptions options =
OutStreamOptions.defaults().setBlockSizeBytes(BLOCK_LENGTH)
.setWriteType(WriteType.MUST_CACHE);
BlockOutStream stream = mock(BlockOutStream.class);
when(mBlockStore.getOutStream(anyInt(), anyLong(), any(OutStreamOptions.class)))
.thenReturn(stream);
mTestStream = createTestStream(FILE_NAME, options);
when(stream.remaining()).thenReturn(BLOCK_LENGTH);
doThrow(new IOException("test error")).when(stream).write((byte) 7);
try {
mTestStream.write(7);
Assert.fail("the test should fail");
} catch (IOException e) {
Assert.assertEquals(ExceptionMessage.FAILED_CACHE.getMessage("test error"), e.getMessage());
}
}
/**
* Tests that if an exception is thrown by the underlying out stream, and the user is using
* {@link UnderStorageType#SYNC_PERSIST} for their under storage type, the error is recovered
* from by writing the data to the under storage out stream.
*/
@Test
public void cacheWriteExceptionSyncPersist() throws IOException {
BlockOutStream stream = mock(BlockOutStream.class);
when(mBlockStore.getOutStream(anyLong(), anyLong(), any(OutStreamOptions.class)))
.thenReturn(stream);
when(stream.remaining()).thenReturn(BLOCK_LENGTH);
doThrow(new IOException("test error")).when(stream).write((byte) 7);
mTestStream.write(7);
mTestStream.write(8);
Assert.assertArrayEquals(new byte[] {7, 8}, mUnderStorageOutputStream.getWrittenData());
// The cache stream is written to only once - the FileInStream gives up on it after it throws
// the first exception.
verify(stream, times(1)).write(anyByte());
}
/**
* Tests that write only writes a byte.
*/
@Test
public void truncateWrite() throws IOException {
// Only writes the lowest byte
mTestStream.write(0x1fffff00);
mTestStream.write(0x1fffff01);
mTestStream.close();
verifyIncreasingBytesWritten(2);
}
/**
* Tests that the correct exception is thrown when a buffer is written with invalid offset/length.
*/
@Test
public void writeBadBufferOffset() throws IOException {
try {
mTestStream.write(new byte[10], 5, 6);
Assert.fail("buffer write with invalid offset/length should fail");
} catch (IllegalArgumentException e) {
Assert.assertEquals(String.format(PreconditionMessage.ERR_BUFFER_STATE.toString(), 10, 5, 6),
e.getMessage());
}
}
/**
* Tests that writing a null buffer throws the correct exception.
*/
@Test
public void writeNullBuffer() throws IOException {
try {
mTestStream.write(null);
Assert.fail("writing null should fail");
} catch (IllegalArgumentException e) {
Assert.assertEquals(PreconditionMessage.ERR_WRITE_BUFFER_NULL.toString(), e.getMessage());
}
}
/**
* Tests that writing a null buffer with offset/length information throws the correct exception.
*/
@Test
public void writeNullBufferOffset() throws IOException {
try {
mTestStream.write(null, 0, 0);
Assert.fail("writing null should fail");
} catch (IllegalArgumentException e) {
Assert.assertEquals(PreconditionMessage.ERR_WRITE_BUFFER_NULL.toString(), e.getMessage());
}
}
/**
* Tests that the async write invokes the expected client APIs.
*/
@Test
public void asyncWrite() throws Exception {
OutStreamOptions options =
OutStreamOptions.defaults().setBlockSizeBytes(BLOCK_LENGTH)
.setWriteType(WriteType.ASYNC_THROUGH);
mTestStream = createTestStream(FILE_NAME, options);
mTestStream.write(BufferUtils.getIncreasingByteArray((int) (BLOCK_LENGTH * 1.5)));
mTestStream.close();
verify(mFileSystemMasterClient).completeFile(eq(FILE_NAME), any(CompleteFileOptions.class));
verify(mFileSystemMasterClient).scheduleAsyncPersist(eq(FILE_NAME));
}
/**
* Tests that the number of bytes written is correct when the stream is created with different
* under storage types.
*/
@Test
public void getBytesWrittenWithDifferentUnderStorageType() throws IOException {
for (WriteType type : WriteType.values()) {
OutStreamOptions options =
OutStreamOptions.defaults().setBlockSizeBytes(BLOCK_LENGTH).setWriteType(type)
.setUfsPath(FILE_NAME.getPath());
mTestStream = createTestStream(FILE_NAME, options);
mTestStream.write(BufferUtils.getIncreasingByteArray((int) BLOCK_LENGTH));
mTestStream.flush();
Assert.assertEquals(BLOCK_LENGTH, mTestStream.getBytesWritten());
}
}
private void verifyIncreasingBytesWritten(int len) {
verifyIncreasingBytesWritten(0, len);
}
/**
* Verifies that the out streams have had exactly `len` increasing bytes written to them, with the
* first byte starting at `start`. Also verifies that the same bytes have been written to the
* under storage file stream.
*/
private void verifyIncreasingBytesWritten(int start, int len) {
long filledStreams = len / BLOCK_LENGTH;
for (long streamIndex = 0; streamIndex < filledStreams; streamIndex++) {
Assert.assertTrue("stream " + streamIndex + " was never written",
mAlluxioOutStreamMap.containsKey(streamIndex));
Assert.assertArrayEquals(BufferUtils
.getIncreasingByteArray((int) (streamIndex * BLOCK_LENGTH + start), (int) BLOCK_LENGTH),
mAlluxioOutStreamMap.get(streamIndex).getWrittenData());
}
long lastStreamBytes = len - filledStreams * BLOCK_LENGTH;
Assert.assertArrayEquals(
BufferUtils.getIncreasingByteArray((int) (filledStreams * BLOCK_LENGTH + start),
(int) lastStreamBytes),
mAlluxioOutStreamMap.get(filledStreams).getWrittenData());
Assert.assertArrayEquals(BufferUtils.getIncreasingByteArray(start, len),
mUnderStorageOutputStream.getWrittenData());
}
/**
* Creates a {@link FileOutStream} for test.
*
* @param path the file path
* @param options the set of options specific to this operation
* @return a {@link FileOutStream}
*/
private FileOutStream createTestStream(AlluxioURI path, OutStreamOptions options)
throws IOException {
return new FileOutStream(path, options, mFileSystemContext);
}
}