/*
* 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.worker.block;
import alluxio.Configuration;
import alluxio.ConfigurationTestUtils;
import alluxio.PropertyKey;
import alluxio.collections.ConcurrentHashSet;
import alluxio.exception.BlockDoesNotExistException;
import alluxio.exception.ExceptionMessage;
import alluxio.exception.InvalidWorkerStateException;
import com.google.common.base.Throwables;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CyclicBarrier;
/**
* Unit tests for {@link BlockLockManager}.
*/
@RunWith(PowerMockRunner.class)
@PrepareForTest(BlockMetadataManager.class)
public final class BlockLockManagerTest {
private static final long TEST_SESSION_ID = 2;
private static final long TEST_BLOCK_ID = 9;
private BlockLockManager mLockManager;
/** Rule to create a new temporary folder during each test. */
@Rule
public TemporaryFolder mFolder = new TemporaryFolder();
/** The exception expected to be thrown. */
@Rule
public ExpectedException mThrown = ExpectedException.none();
/**
* Sets up all dependencies before a test runs.
*/
@Before
public void before() throws Exception {
BlockMetadataManager mockMetaManager = PowerMockito.mock(BlockMetadataManager.class);
PowerMockito.when(mockMetaManager.hasBlockMeta(TEST_BLOCK_ID)).thenReturn(true);
mLockManager = new BlockLockManager();
}
@After
public void after() throws Exception {
ConfigurationTestUtils.resetConfiguration();
}
/**
* Tests the {@link BlockLockManager#lockBlock(long, long, BlockLockType)} method.
*/
@Test
public void lockBlock() {
// Read-lock on can both get through
long lockId1 = mLockManager.lockBlock(TEST_SESSION_ID, TEST_BLOCK_ID, BlockLockType.READ);
long lockId2 = mLockManager.lockBlock(TEST_SESSION_ID, TEST_BLOCK_ID, BlockLockType.READ);
Assert.assertNotEquals(lockId1, lockId2);
}
/**
* Tests that an exception is thrown when trying to unlock a block via
* {@link BlockLockManager#unlockBlockNoException(long)} which is not locked.
*/
@Test
public void unlockNonExistingLock() throws Exception {
long badLockId = 1;
// Unlock a non-existing lockId, expect to see IOException
Assert.assertFalse(mLockManager.unlockBlockNoException(badLockId));
}
/**
* Tests that an exception is thrown when trying to validate a lock of a block via
* {@link BlockLockManager#validateLock(long, long, long)} which is not locked.
*/
@Test
public void validateLockIdWithNoRecord() throws Exception {
long badLockId = 1;
mThrown.expect(BlockDoesNotExistException.class);
mThrown.expectMessage(ExceptionMessage.LOCK_RECORD_NOT_FOUND_FOR_LOCK_ID.getMessage(badLockId));
// Validate a non-existing lockId, expect to see IOException
mLockManager.validateLock(TEST_SESSION_ID, TEST_BLOCK_ID, badLockId);
}
/**
* Tests that an exception is thrown when trying to validate a lock of a block via
* {@link BlockLockManager#validateLock(long, long, long)} with an incorrect session ID.
*/
@Test
public void validateLockIdWithWrongSessionId() throws Exception {
long lockId = mLockManager.lockBlock(TEST_SESSION_ID, TEST_BLOCK_ID, BlockLockType.READ);
long wrongSessionId = TEST_SESSION_ID + 1;
mThrown.expect(InvalidWorkerStateException.class);
mThrown.expectMessage(ExceptionMessage.LOCK_ID_FOR_DIFFERENT_SESSION.getMessage(lockId,
TEST_SESSION_ID, wrongSessionId));
// Validate an existing lockId with wrong session id, expect to see IOException
mLockManager.validateLock(wrongSessionId, TEST_BLOCK_ID, lockId);
}
/**
* Tests that an exception is thrown when trying to validate a lock of a block via
* {@link BlockLockManager#validateLock(long, long, long)} with an incorrect block ID.
*/
@Test
public void validateLockIdWithWrongBlockId() throws Exception {
long lockId = mLockManager.lockBlock(TEST_SESSION_ID, TEST_BLOCK_ID, BlockLockType.READ);
long wrongBlockId = TEST_BLOCK_ID + 1;
mThrown.expect(InvalidWorkerStateException.class);
mThrown.expectMessage(ExceptionMessage.LOCK_ID_FOR_DIFFERENT_BLOCK.getMessage(lockId,
TEST_BLOCK_ID, wrongBlockId));
// Validate an existing lockId with wrong block id, expect to see IOException
mLockManager.validateLock(TEST_SESSION_ID, wrongBlockId, lockId);
}
/**
* Tests that an exception is thrown when trying to validate a lock of a block via
* {@link BlockLockManager#validateLock(long, long, long)} after the session was cleaned up.
*/
@Test
public void cleanupSession() throws Exception {
long sessionId1 = TEST_SESSION_ID;
long sessionId2 = TEST_SESSION_ID + 1;
long lockId1 = mLockManager.lockBlock(sessionId1, TEST_BLOCK_ID, BlockLockType.READ);
long lockId2 = mLockManager.lockBlock(sessionId2, TEST_BLOCK_ID, BlockLockType.READ);
mThrown.expect(BlockDoesNotExistException.class);
mThrown.expectMessage(ExceptionMessage.LOCK_RECORD_NOT_FOUND_FOR_LOCK_ID.getMessage(lockId2));
mLockManager.cleanupSession(sessionId2);
// Expect validating sessionId1 to get through
mLockManager.validateLock(sessionId1, TEST_BLOCK_ID, lockId1);
// Because sessionId2 has been cleaned up, expect validating sessionId2 to throw IOException
mLockManager.validateLock(sessionId2, TEST_BLOCK_ID, lockId2);
}
/**
* Tests that up to WORKER_TIERED_STORE_BLOCK_LOCKS block locks can be grabbed simultaneously.
*/
@Test(timeout = 10000)
public void grabManyLocks() throws Exception {
int maxLocks = 100;
setMaxLocks(maxLocks);
BlockLockManager manager = new BlockLockManager();
for (int i = 0; i < maxLocks; i++) {
manager.lockBlock(i, i, BlockLockType.WRITE);
}
lockExpectingHang(manager, 101);
}
/**
* Tests that an exception is thrown when a session tries to acquire a write lock on a block that
* it currently has a read lock on.
*/
@Test
public void lockAlreadyReadLockedBlock() {
BlockLockManager manager = new BlockLockManager();
manager.lockBlock(1, 1, BlockLockType.READ);
mThrown.expect(IllegalStateException.class);
manager.lockBlock(1, 1, BlockLockType.WRITE);
}
/**
* Tests that an exception is thrown when a session tries to acquire a write lock on a block that
* it currently has a write lock on.
*/
@Test
public void lockAlreadyWriteLockedBlock() {
BlockLockManager manager = new BlockLockManager();
manager.lockBlock(1, 1, BlockLockType.WRITE);
mThrown.expect(IllegalStateException.class);
manager.lockBlock(1, 1, BlockLockType.WRITE);
}
/**
* Tests that two sessions can both take a read lock on the same block.
*/
@Test(timeout = 10000)
public void lockAcrossSessions() throws Exception {
BlockLockManager manager = new BlockLockManager();
manager.lockBlock(1, TEST_BLOCK_ID, BlockLockType.READ);
manager.lockBlock(2, TEST_BLOCK_ID, BlockLockType.READ);
}
/**
* Tests that a write lock can't be taken while a read lock is held.
*/
@Test(timeout = 10000)
public void readBlocksWrite() throws Exception {
BlockLockManager manager = new BlockLockManager();
manager.lockBlock(1, TEST_BLOCK_ID, BlockLockType.READ);
lockExpectingHang(manager, TEST_BLOCK_ID);
}
/**
* Tests that block locks are returned to the pool when they are no longer in use.
*/
@Test(timeout = 10000)
public void reuseLock() throws Exception {
setMaxLocks(1);
BlockLockManager manager = new BlockLockManager();
long lockId1 = manager.lockBlock(TEST_SESSION_ID, 1, BlockLockType.WRITE);
Assert.assertTrue(
manager.unlockBlockNoException(lockId1)); // Without this line the next lock would hang.
manager.lockBlock(TEST_SESSION_ID, 2, BlockLockType.WRITE);
}
/**
* Tests that block locks are not returned to the pool when they are still in use.
*/
@Test(timeout = 10000)
public void dontReuseLock() throws Exception {
setMaxLocks(1);
final BlockLockManager manager = new BlockLockManager();
long lockId1 = manager.lockBlock(TEST_SESSION_ID, 1, BlockLockType.READ);
manager.lockBlock(TEST_SESSION_ID, 1, BlockLockType.READ);
Assert.assertTrue(manager.unlockBlockNoException(lockId1));
lockExpectingHang(manager, 2);
}
/**
* Calls {@link BlockLockManager#lockBlock(long, long, BlockLockType)} and fails if it doesn't
* hang.
*
* @param manager the manager to call lock on
* @param blockId block id to try locking
*/
private void lockExpectingHang(final BlockLockManager manager, final long blockId)
throws Exception {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
manager.lockBlock(TEST_SESSION_ID, blockId, BlockLockType.WRITE);
}
});
thread.start();
thread.join(200);
// Locking should not take 200ms unless there is a hang.
Assert.assertTrue(thread.isAlive());
}
/**
* Tests that taking and releasing many block locks concurrently won't cause a failure.
*
* This is done by creating 200 threads, 100 for each of 2 different block ids. Each thread locks
* and then unlocks its block 50 times. After this, it takes a final lock on its block before
* returning. At the end of the test, the internal state of the lock manager is validated.
*/
@Test(timeout = 10000)
public void stress() throws Throwable {
final int numBlocks = 2;
final int threadsPerBlock = 100;
final int lockUnlocksPerThread = 50;
setMaxLocks(numBlocks);
final BlockLockManager manager = new BlockLockManager();
final List<Thread> threads = new ArrayList<>();
final CyclicBarrier barrier = new CyclicBarrier(numBlocks * threadsPerBlock);
// If there are exceptions, we will store them here.
final ConcurrentHashSet<Throwable> failedThreadThrowables = new ConcurrentHashSet<>();
Thread.UncaughtExceptionHandler exceptionHandler = new Thread.UncaughtExceptionHandler() {
public void uncaughtException(Thread th, Throwable ex) {
failedThreadThrowables.add(ex);
}
};
for (int blockId = 0; blockId < numBlocks; blockId++) {
final int finalBlockId = blockId;
for (int i = 0; i < threadsPerBlock; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
barrier.await();
} catch (Exception e) {
throw new RuntimeException(e);
}
// Lock and unlock the block lockUnlocksPerThread times.
for (int j = 0; j < lockUnlocksPerThread; j++) {
long lockId = manager.lockBlock(TEST_SESSION_ID, finalBlockId, BlockLockType.READ);
Assert.assertTrue(manager.unlockBlockNoException(lockId));
}
// Lock the block one last time.
manager.lockBlock(TEST_SESSION_ID, finalBlockId, BlockLockType.READ);
}
});
t.setUncaughtExceptionHandler(exceptionHandler);
threads.add(t);
}
}
Collections.shuffle(threads);
for (Thread t : threads) {
t.start();
}
for (Thread t : threads) {
t.join();
}
if (!failedThreadThrowables.isEmpty()) {
StringBuilder sb = new StringBuilder("Failed with the following errors:\n");
for (Throwable failedThreadThrowable : failedThreadThrowables) {
sb.append(Throwables.getStackTraceAsString(failedThreadThrowable));
}
Assert.fail(sb.toString());
}
manager.validate();
}
private void setMaxLocks(int maxLocks) {
Configuration.set(PropertyKey.WORKER_TIERED_STORE_BLOCK_LOCKS, Integer.toString(maxLocks));
}
}