package org.threadly.concurrent.wrapper;
import static org.junit.Assert.*;
import static org.threadly.TestConstants.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.threadly.BlockingTestRunnable;
import org.threadly.ThreadlyTestUtil;
import org.threadly.concurrent.DoNothingRunnable;
import org.threadly.concurrent.PriorityScheduler;
import org.threadly.concurrent.StrictPriorityScheduler;
import org.threadly.concurrent.SubmitterExecutor;
import org.threadly.concurrent.TestCallable;
import org.threadly.concurrent.TestRuntimeFailureRunnable;
import org.threadly.concurrent.UnfairExecutor;
import org.threadly.concurrent.lock.StripedLock;
import org.threadly.concurrent.wrapper.limiter.ExecutorLimiter;
import org.threadly.test.concurrent.TestCondition;
import org.threadly.test.concurrent.TestRunnable;
import org.threadly.test.concurrent.TestUtils;
import org.threadly.util.ExceptionUtils;
import org.threadly.util.SuppressedStackRuntimeException;
import org.threadly.util.TestExceptionHandler;
@SuppressWarnings("javadoc")
public class KeyDistributedExecutorTest {
private static final int PARALLEL_LEVEL = TEST_QTY;
private static final int RUNNABLE_COUNT_PER_LEVEL = TEST_QTY * 2;
protected static UnfairExecutor executor;
@BeforeClass
public static void setupClass() {
ThreadlyTestUtil.setIgnoreExceptionHandler();
executor = new UnfairExecutor((PARALLEL_LEVEL * 2) + 1);
}
@AfterClass
public static void cleanupClass() {
executor.shutdownNow();
executor = null;
}
protected Object agentLock;
protected KeyDistributedExecutor distributor;
@Before
public void setup() {
StripedLock sLock = new StripedLock(1);
agentLock = sLock.getLock(null); // there should be only one lock
distributor = new KeyDistributedExecutor(executor, sLock, Integer.MAX_VALUE, false);
}
@After
public void cleanup() {
agentLock = null;
distributor = null;
}
protected List<KDRunnable> populate(AddHandler ah) {
final List<KDRunnable> runs = new ArrayList<>(PARALLEL_LEVEL * RUNNABLE_COUNT_PER_LEVEL);
// hold agent lock to prevent execution till ready
synchronized (agentLock) {
for (int i = 0; i < PARALLEL_LEVEL; i++) {
ThreadContainer tc = new ThreadContainer();
KDRunnable previous = null;
for (int j = 0; j < RUNNABLE_COUNT_PER_LEVEL; j++) {
KDRunnable tr = new KDRunnable(tc, previous);
runs.add(tr);
ah.addTDRunnable(tc, tr);
previous = tr;
}
}
}
return runs;
}
@SuppressWarnings("unused")
@Test
public void constructorTest() {
// none should throw exception
new KeyDistributedExecutor(executor);
new KeyDistributedExecutor(executor, true);
new KeyDistributedExecutor(executor, 1);
new KeyDistributedExecutor(executor, 1, true);
new KeyDistributedExecutor(1, executor);
new KeyDistributedExecutor(1, executor, true);
new KeyDistributedExecutor(1, executor, 1);
new KeyDistributedExecutor(1, executor, 1, true);
StripedLock sLock = new StripedLock(1);
new KeyDistributedExecutor(executor, sLock, 1, false);
}
@SuppressWarnings("unused")
@Test
public void constructorFail() {
try {
new KeyDistributedExecutor(1, null);
fail("Exception should have been thrown");
} catch (IllegalArgumentException e) {
// expected
}
try {
new KeyDistributedExecutor(executor, null,
Integer.MAX_VALUE, false);
fail("Exception should have been thrown");
} catch (IllegalArgumentException e) {
// expected
}
try {
new KeyDistributedExecutor(executor, new StripedLock(1), -1, false);
fail("Exception should have been thrown");
} catch (IllegalArgumentException e) {
// expected
}
}
@Test
public void getExecutorTest() {
assertTrue(executor == distributor.getExecutor());
}
@Test
public void keyBasedSubmitterConsistentThreadTest() {
List<KDRunnable> runs = populate(new AddHandler() {
@Override
public void addTDRunnable(Object key, KDRunnable tdr) {
SubmitterExecutor keySubmitter = distributor.getExecutorForKey(key);
keySubmitter.submit(tdr);
}
});
Iterator<KDRunnable> it = runs.iterator();
while (it.hasNext()) {
KDRunnable tr = it.next();
tr.blockTillFinished(1000 * 20);
assertEquals(1, tr.getRunCount()); // verify each only ran once
assertTrue(tr.threadTracker.threadConsistent()); // verify that all threads for a given key ran in the same thread
assertTrue(tr.previousRanFirst()); // verify runnables were run in order
}
}
@Test (expected = IllegalArgumentException.class)
public void getExecutorForKeyFail() {
distributor.getExecutorForKey(null);
}
@Test
public void executeFail() {
try {
distributor.execute(null, DoNothingRunnable.instance());
fail("Exception should have been thrown");
} catch (IllegalArgumentException e) {
// expected
}
try {
distributor.execute(new Object(), null);
fail("Exception should have been thrown");
} catch (IllegalArgumentException e) {
// expected
}
}
@Test
public void executeConsistentThreadTest() {
List<KDRunnable> runs = populate(new AddHandler() {
@Override
public void addTDRunnable(Object key, KDRunnable tdr) {
distributor.execute(key, tdr);
}
});
Iterator<KDRunnable> it = runs.iterator();
while (it.hasNext()) {
KDRunnable tr = it.next();
tr.blockTillFinished(20 * 1000);
assertEquals(1, tr.getRunCount()); // verify each only ran once
assertTrue(tr.threadTracker.threadConsistent()); // verify that all threads for a given key ran in the same thread
assertTrue(tr.previousRanFirst()); // verify runnables were run in order
}
}
@Test
public void submitRunnableFail() {
try {
distributor.submit(null, DoNothingRunnable.instance());
fail("Exception should have thrown");
} catch (IllegalArgumentException e) {
// expected
}
try {
distributor.submit(new Object(), null, null);
fail("Exception should have thrown");
} catch (IllegalArgumentException e) {
// expected
}
}
@Test
public void submitRunnableConsistentThreadTest() {
List<KDRunnable> runs = populate(new AddHandler() {
@Override
public void addTDRunnable(Object key, KDRunnable tdr) {
distributor.submit(key, tdr);
}
});
Iterator<KDRunnable> it = runs.iterator();
while (it.hasNext()) {
KDRunnable tr = it.next();
tr.blockTillFinished(20 * 1000);
assertEquals(1, tr.getRunCount()); // verify each only ran once
assertTrue(tr.threadTracker.threadConsistent()); // verify that all threads for a given key ran in the same thread
assertTrue(tr.previousRanFirst()); // verify runnables were run in order
}
}
@Test
public void submitCallableConsistentThreadTest() {
List<KDCallable> runs = new ArrayList<>(PARALLEL_LEVEL * RUNNABLE_COUNT_PER_LEVEL);
// hold agent lock to avoid execution till all are submitted
synchronized (agentLock) {
for (int i = 0; i < PARALLEL_LEVEL; i++) {
ThreadContainer tc = new ThreadContainer();
KDCallable previous = null;
for (int j = 0; j < RUNNABLE_COUNT_PER_LEVEL; j++) {
KDCallable tr = new KDCallable(tc, previous);
runs.add(tr);
distributor.submit(tc, tr);
previous = tr;
}
}
}
Iterator<KDCallable> it = runs.iterator();
while (it.hasNext()) {
KDCallable tr = it.next();
tr.blockTillFinished(20 * 1000);
assertTrue(tr.threadTracker.threadConsistent()); // verify that all threads for a given key ran in the same thread
assertTrue(tr.previousRanFirst()); // verify runnables were run in order
}
}
@Test
public void submitCallableFail() {
try {
distributor.submit(null, new TestCallable());
fail("Exception should have thrown");
} catch (IllegalArgumentException e) {
// expected
}
try {
distributor.submit(new Object(), (Callable<Void>)null);
fail("Exception should have thrown");
} catch (IllegalArgumentException e) {
// expected
}
}
@Test
public void executeStressTest() {
final Object testLock = new Object();
final int expectedCount = (PARALLEL_LEVEL * 2) * (RUNNABLE_COUNT_PER_LEVEL * 2);
final List<KDRunnable> runs = new ArrayList<>(expectedCount);
// we can't use populate here because we don't want to hold the agentLock
executor.execute(new Runnable() {
private final Map<Integer, ThreadContainer> containers = new HashMap<>();
private final Map<Integer, KDRunnable> previousRunnables = new HashMap<>();
@Override
public void run() {
synchronized (testLock) {
for (int i = 0; i < RUNNABLE_COUNT_PER_LEVEL * 2; i++) {
for (int j = 0; j < PARALLEL_LEVEL * 2; j++) {
ThreadContainer tc = containers.get(j);
if (tc == null) {
tc = new ThreadContainer();
containers.put(j, tc);
}
KDRunnable tr = new KDRunnable(tc, previousRunnables.get(j)) {
private boolean added = false;
@Override
public void handleRunFinish() {
if (! added) {
distributor.execute(threadTracker, this);
added = true;
}
}
};
runs.add(tr);
distributor.execute(tc, tr);
previousRunnables.put(j, tr);
}
}
}
}
});
// block till ready to ensure other thread got testLock lock
new TestCondition() {
@Override
public boolean get() {
synchronized (testLock) {
return runs.size() == expectedCount;
}
}
}.blockTillTrue(20 * 1000, 100);
synchronized (testLock) {
Iterator<KDRunnable> it = runs.iterator();
while (it.hasNext()) {
KDRunnable tr = it.next();
tr.blockTillFinished(20 * 1000, 2);
assertEquals(2, tr.getRunCount()); // verify each only ran twice
assertTrue(tr.previousRanFirst); // verify runnables were run in order
assertFalse(tr.ranConcurrently()); // verify that it never run in parallel
}
}
}
@Test
public void taskExceptionTest() {
Integer key = 1;
TestExceptionHandler teh = new TestExceptionHandler();
final RuntimeException testException = new SuppressedStackRuntimeException();
ExceptionUtils.setDefaultExceptionHandler(teh);
TestRunnable exceptionRunnable = new TestRuntimeFailureRunnable(testException);
TestRunnable followRunnable = new TestRunnable();
distributor.execute(key, exceptionRunnable);
distributor.execute(key, followRunnable);
exceptionRunnable.blockTillFinished();
followRunnable.blockTillStarted(); // verify that it ran despite the exception
assertEquals(1, teh.getCallCount());
assertEquals(testException, teh.getLastThrowable());
}
@Test
public void limitExecutionPerCycleTest() {
final AtomicInteger execCount = new AtomicInteger(0);
KeyDistributedExecutor distributor = new KeyDistributedExecutor(1, new Executor() {
@Override
public void execute(Runnable command) {
execCount.incrementAndGet();
new Thread(command).start();
}
}, 1);
BlockingTestRunnable btr = new BlockingTestRunnable();
distributor.execute(this, btr);
btr.blockTillStarted();
// add second task while we know worker is active
TestRunnable secondTask = new TestRunnable();
distributor.execute(this, secondTask);
assertEquals(1, distributor.taskWorkers.size());
assertEquals(1, distributor.taskWorkers.get(this).queue.size());
btr.unblock();
secondTask.blockTillFinished();
// verify worker execed out between task
assertEquals(2, execCount.get());
}
@Test
public void limitExecutionPerCycleStressTest() {
PriorityScheduler scheduler = new StrictPriorityScheduler(3);
final AtomicBoolean testComplete = new AtomicBoolean(false);
try {
final Integer key1 = 1;
final Integer key2 = 2;
Executor singleThreadedExecutor = new ExecutorLimiter(scheduler, 1);
final KeyDistributedExecutor distributor = new KeyDistributedExecutor(2, singleThreadedExecutor, 2);
final AtomicInteger waitingTasks = new AtomicInteger();
final AtomicReference<TestRunnable> lastTestRunnable = new AtomicReference<>();
scheduler.execute(new Runnable() { // execute thread to add for key 1
@Override
public void run() {
while (! testComplete.get()) {
TestRunnable next = new TestRunnable() {
@Override
public void handleRunStart() {
waitingTasks.decrementAndGet();
TestUtils.sleep(20); // wait to make sure producer is faster than executor
}
};
lastTestRunnable.set(next);
waitingTasks.incrementAndGet();
distributor.execute(key1, next);
}
}
});
// block till there is for sure a backup of key1 tasks
new TestCondition(() -> waitingTasks.get() > 10).blockTillTrue();
TestRunnable key2Runnable = new TestRunnable();
distributor.execute(key2, key2Runnable);
TestRunnable lastKey1Runnable = lastTestRunnable.get();
key2Runnable.blockTillStarted(); // will throw exception if not started
// verify it ran before the lastKey1Runnable
assertFalse(lastKey1Runnable.ranOnce());
} finally {
testComplete.set(true);
scheduler.shutdownNow();
}
}
private static void getTaskQueueSizeSimpleTest(boolean accurateDistributor) {
final Object taskKey = new Object();
KeyDistributedExecutor kde = new KeyDistributedExecutor(new Executor() {
@Override
public void execute(Runnable command) {
// kidding, don't actually execute, haha
}
}, accurateDistributor);
assertEquals(0, kde.getTaskQueueSize(taskKey));
kde.execute(taskKey, DoNothingRunnable.instance());
// should add as first task
assertEquals(1, kde.getTaskQueueSize(taskKey));
kde.execute(taskKey, DoNothingRunnable.instance());
// will now add into the queue
assertEquals(2, kde.getTaskQueueSize(taskKey));
}
private static void getTaskQueueSizeThreadedTest(boolean accurateDistributor) {
final Object taskKey = new Object();
KeyDistributedExecutor kde = new KeyDistributedExecutor(executor, accurateDistributor);
assertEquals(0, kde.getTaskQueueSize(taskKey));
BlockingTestRunnable btr = new BlockingTestRunnable();
kde.execute(taskKey, btr);
// add more tasks while remaining blocked
kde.execute(taskKey, DoNothingRunnable.instance());
kde.execute(taskKey, DoNothingRunnable.instance());
btr.blockTillStarted();
assertEquals(2, kde.getTaskQueueSize(taskKey));
btr.unblock();
}
@Test
public void getTaskQueueSizeInaccurateTest() {
getTaskQueueSizeSimpleTest(false);
getTaskQueueSizeThreadedTest(false);
}
@Test
public void getTaskQueueSizeAccurateTest() {
getTaskQueueSizeSimpleTest(true);
getTaskQueueSizeThreadedTest(true);
}
private static void getTaskQueueSizeMapSimpleTest(boolean accurateDistributor) {
final Object taskKey = new Object();
KeyDistributedExecutor kde = new KeyDistributedExecutor(new Executor() {
@Override
public void execute(Runnable command) {
// kidding, don't actually execute, haha
}
}, accurateDistributor);
Map<?, Integer> result = kde.getTaskQueueSizeMap();
assertTrue(result.isEmpty());
kde.execute(taskKey, DoNothingRunnable.instance());
// should add as first task
result = kde.getTaskQueueSizeMap();
assertEquals(1, result.size());
assertEquals((Integer)1, result.get(taskKey));
kde.execute(taskKey, DoNothingRunnable.instance());
// will now add into the queue
result = kde.getTaskQueueSizeMap();
assertEquals(1, result.size());
assertEquals((Integer)2, result.get(taskKey));
}
private static void getTaskQueueSizeMapThreadedTest(boolean accurateDistributor) {
final Object taskKey = new Object();
KeyDistributedExecutor kde = new KeyDistributedExecutor(executor, accurateDistributor);
Map<?, Integer> result = kde.getTaskQueueSizeMap();
assertTrue(result.isEmpty());
BlockingTestRunnable btr = new BlockingTestRunnable();
kde.execute(taskKey, btr);
// add more tasks while remaining blocked
kde.execute(taskKey, DoNothingRunnable.instance());
kde.execute(taskKey, DoNothingRunnable.instance());
btr.blockTillStarted();
result = kde.getTaskQueueSizeMap();
assertEquals(1, result.size());
assertEquals((Integer)2, result.get(taskKey));
btr.unblock();
}
@Test
public void getTaskQueueSizeMapInaccurateTest() {
getTaskQueueSizeMapSimpleTest(false);
getTaskQueueSizeMapThreadedTest(false);
}
@Test
public void getTaskQueueSizeMapAccurateTest() {
getTaskQueueSizeMapSimpleTest(true);
getTaskQueueSizeMapThreadedTest(true);
}
public static class KDRunnable extends TestRunnable {
public final KDRunnable previousRunnable;
public final ThreadContainer threadTracker;
private volatile boolean previousRanFirst;
private volatile boolean verifiedPrevious;
public KDRunnable(ThreadContainer threadTracker, KDRunnable previousRunnable) {
this.threadTracker = threadTracker;
this.previousRunnable = previousRunnable;
previousRanFirst = false;
verifiedPrevious = false;
}
@Override
public void handleRunStart() {
threadTracker.running();
if (! verifiedPrevious) {
if (previousRunnable != null) {
previousRanFirst = previousRunnable.getRunCount() >= 1;
} else {
previousRanFirst = true;
}
verifiedPrevious = true;
}
}
public boolean previousRanFirst() {
return previousRanFirst;
}
}
public static class KDCallable extends TestCallable {
public final KDCallable previousRunnable;
public final ThreadContainer threadTracker;
private volatile boolean previousRanFirst;
private volatile boolean verifiedPrevious;
public KDCallable(ThreadContainer threadTracker, KDCallable previousRunnable) {
this.threadTracker = threadTracker;
this.previousRunnable = previousRunnable;
previousRanFirst = false;
verifiedPrevious = false;
}
@Override
public void handleCallStart() {
threadTracker.running();
if (! verifiedPrevious) {
if (previousRunnable != null) {
previousRanFirst = previousRunnable.isDone();
} else {
previousRanFirst = true;
}
verifiedPrevious = true;
}
}
public boolean previousRanFirst() {
return previousRanFirst;
}
}
public static class ThreadContainer {
private Thread runningThread = null;
private boolean threadConsistent = true;
public synchronized void running() {
if (runningThread == null) {
runningThread = Thread.currentThread();
} else {
threadConsistent = threadConsistent && runningThread == Thread.currentThread();
}
}
public boolean threadConsistent() {
return threadConsistent;
}
}
protected interface AddHandler {
public void addTDRunnable(Object key, KDRunnable tdr);
}
}