I have written the following Executor:
/**
* <p>This executor guarantees that there is never more than one element waiting to be executed. If an element is
* submitted for execution and another element is still in queue, the newly submitted element replaces the previously
* queued element in the queue</p>
*
* <p>Additionally this executor sleeps a constructor specified number of milliseconds before executing tasks to give queued
* items a chance to be canceled</p>
*
* <p>This class is intended to be used when runnables are created in rapid succession and newer runnables supersede
* ones that have been created previously. This is e.g. the case if a user interface reacts to user inputs, possibly
* on every keystroke and the reaction causes slow tasks (e.g. server calls for validation).</p>
*/
public class SingleQueuedElementExecutor implements Executor {
private final Sleeper sleeper;
private final ExecutorService delegate;
private Future<?> queuedSleeper;
private Future<?> queuedElement;
public SingleQueuedElementExecutor(final ExecutorService delegate, final long sleepBeforeExecutionMilliseconds) {
this.delegate = delegate;
sleeper = new Sleeper(sleepBeforeExecutionMilliseconds);
}
public void execute(final Runnable runnable) {
if (queuedElement != null) {
queuedElement.cancel(false);
queuedSleeper.cancel(true);
}
queuedSleeper = delegate.submit(sleeper);
queuedElement = delegate.submit(runnable);
}
private static class Sleeper implements Runnable {
private final long duration;
private Sleeper(final long duration) {
this.duration = duration;
}
public void run() {
try {
Thread.sleep(duration);
} catch (InterruptedException e) {
// Simply abort sleeping
}
}
}
}
I believe that the JavaDoc describes accurately what I expect the class to do.
Now the interesting part which I ask a review about is the test for this class I came up with:
public class SingleQueuedElementExecutorTest {
private SingleQueuedElementExecutor executor;
private ExecutorService delegateExecutor;
private static final long SLEEP_BETWEEN_TASKS_MS = 10;
private static final long NANO_TIME_ACCURACY_MS = 2;
@BeforeMethod
public void setUp() throws Exception {
delegateExecutor = Executors.newSingleThreadExecutor();
executor = new SingleQueuedElementExecutor(delegateExecutor, SLEEP_BETWEEN_TASKS_MS);
}
@Test
public void execute_sleepsBeforeExecuting() throws Exception {
// setup
final InvocationCounter invocationCounter = new InvocationCounter();
// execution
final long startTime = System.nanoTime();
executor.execute(invocationCounter);
waitForTasksToComplete();
final long endTime = System.nanoTime();
// evaluation
assertThat(endTime - startTime)
.describedAs("number of ns execution took")
.isGreaterThanOrEqualTo((SLEEP_BETWEEN_TASKS_MS - NANO_TIME_ACCURACY_MS) * 1000);
}
private void waitForTasksToComplete() throws InterruptedException {
delegateExecutor.shutdown();
if (!delegateExecutor.awaitTermination(30, TimeUnit.SECONDS)) {
// There is nothing that takes even near 1 second in this test. So if the executor does not terminate within
// 30s, than there is surely an error.
throw new Error("executor did not terminate within 30s.");
}
}
@Test
public void execute_executesTask() throws Exception {
// setup
final InvocationCounter invocationCounter = new InvocationCounter();
// execution
executor.execute(invocationCounter);
waitForTasksToComplete();
// evaluation
assertThat(invocationCounter.invocations)
.isEqualTo(1);
}
@Test
public void execute_abortsPreviouslyAddedTasks() throws Exception {
// setup
final InvocationCounter invocationCounterA = new InvocationCounter();
final InvocationCounter invocationCounterB = new InvocationCounter();
// execution
executor.execute(invocationCounterA);
executor.execute(invocationCounterB);
waitForTasksToComplete();
// evaluation
assertThat(invocationCounterA.invocations)
.describedAs("invocations of counter A")
.isEqualTo(0);
assertThat(invocationCounterB.invocations)
.describedAs("invocations of counter B")
.isEqualTo(1);
}
private static class InvocationCounter implements Runnable {
private int invocations = 0;
public synchronized void run() {
++invocations;
}
}
}
While the test seems to work fine on my machine I don't like the timing parts of the test. The call to delegateExecutor.awaitTermination(30, TimeUnit.SECONDS)
but maybe acceptable. However I strongly dislike my execute_sleepsBeforeExecuting
method which relies on System.nanoTime()
.
What do you think about it? Any suggestions for improvement?