Runnables with Timeout from submission time
Problem not covered by Java ExecutorService
Java’s ThreadPoolExecutor is a great tool. After configuring a thread pool we can keep on submitting Callables and get Futures. These futures can be used to get the values that the callable is supposed to return. In case we want to limit the execution time of this callable we have the option of passing a Timeout. This is a neat tool but has a small problem. Suppose we do
For this blog post I am using pseudo code not actual Java code for clarity.
[code]
Future future = executorService.submit(callable)
future.get(timeout, unit)
[/code]
The timeout being used here will be calculated from the time we call get. So if we have a callable on which we specify a timeout of 20 ms then the 20ms will be calculated from the second line not the first one.
Seems like a small distinction but it’s not. In case we are submitting 100 callables each with 20ms timeout one after other and then calling Future.get one by one on each of the futures then the total timeout can be as large as 2 seconds (20 ms * 100). While we thought that we were using a thread pool and getting the result in maximum 20 ms we might actually be waiting for as large as 2 seconds.
An example code is below. Here even if impose 20ms timeout on each future in the worst case the below can take 60ms.
[code]
Future future1 = executorService.submit(callable)
Future future2 = executorService.submit(callable)
Future future3 = executorService.submit(callable)
future3.get(timeout, unit)
future2.get(timeout, unit)
future1.get(timeout, unit)
[/code]
This seems micro-optimization but it can matter in case you are working on a performance hotspot and there are upper bounds on the maximum acceptable response time. In such a situation you may be making multiple network/IO calls each in parallel and if some service does not respond then fall back to some default value. This distinction can matter in that case. In such a situation using a thread pool with callable and imposing timeouts can be a solution.
So the problem not covered is “not being able to impose timeouts on parallel threads from the time of submission to thread pool”.
Solution – Parallel Runnable with timeouts from submission time
We can implement our own abstract Runnable which does exactly that.
[code]
import java.util.concurrent.TimeUnit;
public abstract class RunnableWithTimeout<V> implements Runnable {
protected long startTimeMillis = System.currentTimeMillis();
protected long timeOutMillis = 0;
protected V result = null;
protected boolean done = false;
protected Exception exception = null;
public RunnableWithTimeout(long timeout, TimeUnit unit) {
timeOutMillis = unit.toMillis(timeout);
}
public Exception getException() {
return exception;
}
public void waitFor()
throws InterruptedException {
synchronized (this) {
while (!done && exception == null) {
long timeLeft = startTimeMillis + timeOutMillis – System.currentTimeMillis();
if (timeLeft > 0) {
wait(timeLeft);
}
}
}
}
public abstract V runNotingCompletion();
public abstract void executeIfNotDoneAfterTimeout();
@Override
public void run() {
try {
result = runNotingCompletion();
done = true;
} catch (Exception e) {
exception = e;
}
synchronized (this) {
notifyAll();
}
}
public V getResultIfPresent() {
try {
waitFor();
} catch (InterruptedException e) {
//Wait
}
if (! done) {
executeIfNotDoneAfterTimeout();
}
return result;
}
}
[/code]
Here we note the start time when the Runnable is created. When we do
[code]
executorService.submit(runnable)
[/code]
The thread pool will call the run method so we have overridden that to do the job taking care of other things.
To use this two methods need to be overridden the runNotingCompletion method which is essentially the run method for anyone using this class and executeIfNotDoneAfterTimeout method which is for the timeout case. If any exception comes that is stored in a variable so that it can be used later.
To get the result need to call getResultIfPresent method. If the method runNotingCompletion returned any result you will get that via this method. You also have the option to look at the exception and do anything you want.