package gobblin.async;
import java.util.Queue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import org.testng.Assert;
import org.testng.annotations.Test;
import gobblin.util.ExponentialBackoff;
@Test
public class AsyncDataDispatcherTest {
/**
* Test successful data dispatch with 2 writers. No exception
*/
public void testSuccessfulDataDispatch()
throws ExecutionException, InterruptedException {
final TestAsyncDataDispatcher dispatcher = new TestAsyncDataDispatcher();
// This should work when there is nothing to process
dispatcher.waitForABufferEmptyOccurrence();
ExecutorService service = Executors.newFixedThreadPool(2);
Writer writer1 = new Writer(dispatcher);
Writer writer2 = new Writer(dispatcher);
Future<?> future1 = service.submit(writer1);
Future<?> future2 = service.submit(writer2);
// Process 10 records
ExponentialBackoff.awaitCondition().callable(new Callable<Boolean>() {
@Override
public Boolean call()
throws Exception {
return dispatcher.count == 10;
}
}).maxWait(1000L).await();
writer1.shouldWaitForABufferEmpty = true;
writer2.shouldWaitForABufferEmpty = true;
dispatcher.count = 0;
// Process another 10 records
ExponentialBackoff.awaitCondition().callable(new Callable<Boolean>() {
@Override
public Boolean call()
throws Exception {
return dispatcher.count == 10;
}
}).maxWait(1000L).await();
writer1.shouldExit = true;
writer2.shouldExit = true;
try {
future1.get();
future2.get();
service.shutdown();
dispatcher.terminate();
} catch (Exception e) {
Assert.fail("Could not complete successful data dispatch");
}
Assert.assertTrue(dispatcher.isDispatchCalled);
Assert.assertTrue(writer1.aBufferEmptyWaited);
Assert.assertTrue(writer2.aBufferEmptyWaited);
}
/**
* Test successful data dispatch with 2 writers. Normal exception
*/
public void testSuccessfulDataDispatchWithNormalException()
throws ExecutionException, InterruptedException {
final TestAsyncDataDispatcher dispatcher = new TestAsyncDataDispatcher();
ExecutorService service = Executors.newFixedThreadPool(2);
Writer writer1 = new Writer(dispatcher);
Writer writer2 = new Writer(dispatcher);
Future<?> future1 = service.submit(writer1);
Future<?> future2 = service.submit(writer2);
dispatcher.status = DispatchStatus.ERROR;
// Process 10 records
ExponentialBackoff.awaitCondition().callable(new Callable<Boolean>() {
@Override
public Boolean call()
throws Exception {
return dispatcher.count == 10;
}
}).maxWait(1000L).await();
writer1.shouldExit = true;
writer2.shouldExit = true;
// Everything should be fine
try {
future1.get();
future2.get();
service.shutdown();
dispatcher.terminate();
} catch (Exception e) {
Assert.fail("Could not complete successful data dispatch");
}
Assert.assertTrue(dispatcher.isDispatchCalled);
}
/**
* Test data dispatch with 2 writers. Fatal exception
*/
public void testSuccessfulDataDispatchWithFatalException()
throws ExecutionException, InterruptedException {
final TestAsyncDataDispatcher dispatcher = new TestAsyncDataDispatcher();
ExecutorService service = Executors.newFixedThreadPool(2);
Writer writer1 = new Writer(dispatcher);
Writer writer2 = new Writer(dispatcher);
Future<?> future1 = service.submit(writer1);
Future<?> future2 = service.submit(writer2);
dispatcher.status = DispatchStatus.FATAL;
// Process 10 records
ExponentialBackoff.awaitCondition().callable(new Callable<Boolean>() {
@Override
public Boolean call()
throws Exception {
return dispatcher.count == 10;
}
}).maxWait(1000L).await();
boolean hasAnException = false;
try {
dispatcher.put(new Object());
} catch (Exception e) {
hasAnException = true;
}
Assert.assertTrue(hasAnException, "put should get an exception");
hasAnException = false;
try {
dispatcher.waitForABufferEmptyOccurrence();
} catch (Exception e) {
hasAnException = true;
}
Assert.assertTrue(hasAnException, "waitForABufferEmptyOccurrence should get an exception");
hasAnException = false;
try {
// Everything should be fine
future1.get();
} catch (ExecutionException e) {
hasAnException = true;
} catch (InterruptedException e) {
// Do nothing
}
Assert.assertTrue(hasAnException, "future1 should get an exception");
hasAnException = false;
try {
// Everything should be fine
future2.get();
} catch (ExecutionException e) {
hasAnException = true;
} catch (InterruptedException e) {
// Do nothing
}
Assert.assertTrue(hasAnException, "future2 should get an exception");
hasAnException = false;
try {
service.shutdown();
dispatcher.terminate();
} catch (Exception e) {
hasAnException = true;
}
Assert.assertTrue(hasAnException, "terminating should get an exception");
Assert.assertTrue(dispatcher.isDispatchCalled);
}
enum DispatchStatus {
// Dispatch success
OK,
// Dispatch exception
ERROR,
// Fatal dispatch exception
FATAL
}
class TestAsyncDataDispatcher extends AsyncDataDispatcher<Object> {
volatile DispatchStatus status;
volatile int count;
boolean isDispatchCalled;
TestAsyncDataDispatcher() {
super(2);
status = DispatchStatus.OK;
isDispatchCalled = false;
count = 0;
}
@Override
protected void dispatch(Queue<Object> buffer)
throws DispatchException {
// Assert buffer must not be empty anytime dispatch is called
Assert.assertTrue(buffer.size() > 0);
isDispatchCalled = true;
// Consume a record
buffer.poll();
count++;
switch (status) {
case OK:
return;
case ERROR:
throw new DispatchException("error", false);
case FATAL:
throw new DispatchException("fatal");
}
}
}
class Writer implements Runnable {
TestAsyncDataDispatcher dispather;
boolean aBufferEmptyWaited;
volatile boolean shouldWaitForABufferEmpty;
volatile boolean shouldExit;
Writer(TestAsyncDataDispatcher dispather) {
this.dispather = dispather;
shouldWaitForABufferEmpty = false;
shouldExit = false;
aBufferEmptyWaited = false;
}
@Override
public void run() {
while (!shouldExit) {
dispather.put(new Object());
if (shouldWaitForABufferEmpty) {
dispather.waitForABufferEmptyOccurrence();
aBufferEmptyWaited = true;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// Do nothing
}
}
}
}
}