package org.ovirt.engine.core.utils.lock;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
/**
* In this test, we need to test, that MacPoolLockingProxy actually works. Assuming write lock, we must setup scenario,
* where two thread may meet inside of 'method to be locked' and if lock is defunct, we must detect it.
* <p>
* To do so, we need to alter object wrapped by this proxy, which is MacPool, so its methods does not finish quickly.
* This is achieved in org.ovirt.engine.core.bll.network.macpool.MacPoolLockingProxyTest#verifyingPoolProxy().
* This method suspends current thread for
* org.ovirt.engine.core.bll.network.macpool.MacPoolLockingProxyTest#METHOD_LOCKED_DURATION giving chance to another
* thread to step into here meantime. This method, aside from stalling, also notes 'integer' value (obtained from
* thread safe atomicInteger field) at its start and end. If there was any other thread communication, 'start' and
* 'end'
* will differ by bigger number than 1. However, we do not have place where to note these numbers, as then cannot be
* stored in 'stalling proxy' (which is shared between threads). Because of that, those numbers are stored into
* org.ovirt.engine.core.bll.network.macpool.MacPoolLockingProxyTest#threadMarks using thread ID as a key.
*/
public class LockedObjectFactoryTest {
@Rule
public ExpectedException expectedException = ExpectedException.none();
//be careful with making this delays smaller or removing them entirely, as surprising result may occur.
/**
* number of millis for which is each decorated method delayed.
*/
private static final int METHOD_LOCKED_DURATION = 100;
/**
* number of millis between 2 tread execution
*/
private static final int DELAY_BETWEEN_THREADS_MILLIS = 20;
private final TestInstance testInstanceA = new TestInstance();
private final TestInstance testInstanceB = new TestInstance();
private final LockedObjectFactory lockedObjectFactory = new LockedObjectFactory();
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private TestInterface lockedTestInstanceA = createLockedTestInstance(testInstanceA);
private TestInterface lockedTestInstanceB = createLockedTestInstance(testInstanceB);
private TestInterface createLockedTestInstance(TestInstance testInstanceA) {
return lockedObjectFactory.createLockingInstance(testInstanceA, TestInterface.class, lock);
}
@Test
public void testMethodWithReadLockWhenBlockedByWriteMethod() throws Exception {
Runnable action = () -> lockedTestInstanceA.methodWithReadLock();
performOperation(action, () -> lockedTestInstanceB.methodWithWriteLock(), false);
}
@Test
public void testMethodWithReadLockWhenAccessedTwice() throws Exception {
performOperation(() -> lockedTestInstanceA.methodWithReadLock(),
() -> lockedTestInstanceB.methodWithReadLock(), true);
}
@Test
public void testMethodWithWriteWhenAccessedTwiceLock() {
performOperation(() -> lockedTestInstanceA.methodWithWriteLock(),
() -> lockedTestInstanceB.methodWithWriteLock(), false);
}
public void testAllocateNewMacWhenBlockedByReadMethod() {
Runnable action = () -> lockedTestInstanceA.methodWithWriteLock();
performOperation(() -> lockedTestInstanceB.methodWithReadLock(), action, false);
}
/**
* @param action1 action for first thread
* @param action2 action for the second thread
* @param expectOverlap expected result; use false to expect threads to be executed in sequential manner.
*/
private void performOperation(Runnable action1, Runnable action2, boolean expectOverlap) {
Thread thread1 = new Thread(action1);
thread1.start();
sleep(DELAY_BETWEEN_THREADS_MILLIS); // give chance thread above to actually start.
Thread thread2 = new Thread(action2);
thread2.start();
//we need to join, so that JUnit will not finish execution before threads are finished.
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
assertThat(threadsOverlaps(), is(expectOverlap));
}
private boolean threadsOverlaps() {
return threadsOverlaps(testInstanceA.getThreadMarks())
|| threadsOverlaps(testInstanceB.getThreadMarks());
}
/**
* @param marks List of integers representing marks of one thread. If those marks
* forms sequence of numbers increasing by 1, then there was no interruption during execution.
*
* @return true if threads intervenes, false otherwise (their execution was sequential).
*/
private boolean threadsOverlaps(List<Integer> marks) {
int marksCount = marks.size();
if (marksCount == 0) {
return false;
}
for (int i = 1; i < marksCount; i++) {
Integer left = marks.get(i - 1);
Integer right = marks.get(i);
if (left + 1 != right) {
return true;
}
}
return false;
}
@Test
public void testThreadsOverlaps() {
assertThat(threadsOverlaps(Arrays.asList(0, 2)), is(true));
assertThat(threadsOverlaps(Arrays.asList(1, 3)), is(true));
assertThat(threadsOverlaps(Arrays.asList(0, 1)), is(false));
assertThat(threadsOverlaps(Arrays.asList(2, 3)), is(false));
}
/**
* Tests, that proxy does not throw controlled exception, if proxied instance throws a RuntimeException.
* Otherwise it would cause caller to fail with UndeclaredThrowableException
*/
@Test
public void testThatNoControlledExceptionIsThrown() {
NullPointerException runtimeException = new NullPointerException();
expectedException.expect(runtimeException.getClass());
lockedTestInstanceA.failingMethod(runtimeException);
}
private static void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public interface TestInterface {
void unlockedMethod();
@AcquireReadLock
void methodWithReadLock();
@AcquireWriteLock
void methodWithWriteLock();
void failingMethod(RuntimeException runtimeException);
}
public static class TestInstance implements TestInterface {
/**
* this is a global counter, used by individual threads, to mark they progress through execution of tested method.
*/
private static AtomicInteger ATOMIC_INTEGER = new AtomicInteger();
private List<Integer> threadMarks = new ArrayList<>();
@Override
public void unlockedMethod() {
methodImpl();
}
@Override
public void methodWithReadLock() {
methodImpl();
}
@Override
public void methodWithWriteLock() {
methodImpl();
}
private void methodImpl() {
markExecutionProgress();
sleep(METHOD_LOCKED_DURATION);
markExecutionProgress();
}
/**
* When called new atomic integer is queried and stored in list to have start and end
* number marks for thread excercising this TestInstance.
*/
private void markExecutionProgress() {
int value = ATOMIC_INTEGER.getAndIncrement();
threadMarks.add(value);
}
public List<Integer> getThreadMarks() {
return threadMarks;
}
@Override
public void failingMethod(RuntimeException runtimeException) {
throw runtimeException;
}
}
}