How do I get an exception from a canceled + interrupted call transferred to a performer?
Consider the following code:
ExecutorService executor = Executors.newSingleThreadExecutor();
final CountDownLatch taskStarted = new CountDownLatch(1);
Future<String> future = executor.submit( new Callable<String>() {
@Override
public synchronized String call() throws Exception {
try {
taskStarted.countDown();
this.wait( 60000 );
return "foo";
}
catch( Exception iWantToGetThisExceptionOutside ) {
iWantToGetThisExceptionOutside.printStackTrace();
throw iWantToGetThisExceptionOutside;
}
}
});
assertTrue(taskStarted.await(60, TimeUnit.SECONDS));
try {
future.get(500, TimeUnit.MILLISECONDS);
fail("Timeout expected.");
} catch (ExecutionException | TimeoutException e) {
e.printStackTrace();
}
future.cancel(true); //mayInterruptIfRunning
//how to get iWantToGetThisExceptionOutside here?
Is there a way to get iWantToGetThisExceptionOutside
in the main thread after canceling? Do I need to create my own performer?
EDIT : Just to make it clear what is ExecutionException
chosen, but TimeoutException
that does not contain any reason. iWantToGetThisExceptionOutside
is normal InterruptedException
.
EDIT2 : A bit of clarification: The task is relatively simple. I want to be able to cancel a task if it takes too long. For this I need a get call
with a timeout that throws an exception on timeout. However, I would welcome a stack entry in my log that shows WHERE the task has been canceled. For this I will need this exception out Callable
.
source to share
You can help yourself with a customizable FutureTask
:
public class TracingFutureTask<T> extends FutureTask<T> {
private Throwable trace;
private boolean done;
public TracingFutureTask(Callable<T> callable) {
super(callable);
}
public TracingFutureTask(Runnable runnable, T result) {
super(runnable, result);
}
@Override
public void run() {
try { super.run(); }
finally { synchronized(this) { done=true; notifyAll(); }}
}
@Override
protected void setException(Throwable t) {
trace=t;
super.setException(t);
}
public synchronized Throwable getException() throws InterruptedException {
while(!done) wait();
return trace;
}
public synchronized Throwable getException(long timeout)
throws InterruptedException, TimeoutException {
for(long deadline = System.currentTimeMillis()+timeout, toWait=timeOut;
!done; toWait = deadline-System.currentTimeMillis()) {
if ( toWait <=0 ) throw new TimeoutException(
"Thread did not end in " + timeout + " milliseconds!" );
wait(toWait);
}
return trace;
}
public static <V> TracingFutureTask<V> submit(Executor e, Callable<V> c) {
TracingFutureTask<V> ft=new TracingFutureTask<>(c);
e.execute(ft);
return ft;
}
public static <V> TracingFutureTask<V> submit(Executor e, Runnable r, V v) {
TracingFutureTask<V> ft=new TracingFutureTask<>(r, v);
e.execute(ft);
return ft;
}
}
This keeps track of the exception in addition to the base class, but unlike the base class, it will remember this even when the job was canceled. This is why there is additional synchronization between the method run()
and getException()
, since in case of cancellation, the job may enter the canceled state (which means "done") before the exception was recorded, so we must submit our own done
with correct synchronization.
It can be used like:
ExecutorService executor = Executors.newSingleThreadExecutor();
TracingFutureTask<String> future=TracingFutureTask.submit(executor, new Callable<String>(){
@Override
public synchronized String call() throws Exception {
this.wait( 60000 );
return "foo";
}
});
try {
future.get(500, TimeUnit.MILLISECONDS);
fail("Timeout expected.");
} catch (ExecutionException | TimeoutException e) {
e.printStackTrace();
}
if(future.cancel(true)) {
System.err.println("cancelled.");
Throwable t = future.getException();
if(t!=null) t.printStackTrace(System.err.append("cancellation caused "));
}
(derived from your example code)
java.util.concurrent.TimeoutException at java.util.concurrent.FutureTask.get(FutureTask.java:205) at so.TestCancel.main(TestCancel.java:69) cancelled. cancellation caused java.lang.InterruptedException at java.lang.Object.wait(Native Method) at so.TestCancel$1.call(TestCancel.java:64) at so.TestCancel$1.call(TestCancel.java:61) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at so.TracingFutureTask.run(TestCancel.java:33) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745)
source to share
You ExecutionException
have to cheat yours at startup iWant...
Exception
.
You can add custom logic by checking Throwable
yours ExecutionException
in your expression catch
on the main thread:
// pseudo-code
if (e.getCause().[something, i.e. getMessage]) {
// TODO something
}
Note
In your implementation, call
you're catch
ing and re throw
with the same Exception
, which doesn't make sense.
Note II
It doesn't really make sense to infer the timeout from the logic being called, although you can always programmatically calculate the time it takes to execute something from start call
to finish call
.
The whole timeout point is TimeoutException
meant for the caller to think that this pending task took too long.
To do this, you are catching TimeoutException
in your expression catch
.
If you need to "decorate" yours TimeoutException
, but for a specific reason causing the execution to take too long, you can:
- programmatically calculate the time from the start of the call
call
to the end of the callcall
and athrow
customException
one that will be wrappedExecutionException
(very ugly), or - Make your own lazy execution of each "subtask" in the method
call
andthrow
customException
for any time
source to share
Instead of relying on throw / catch, you could carry the exception out of the Callable simply as an object (using synchronized shared state). It looks ugly, but it works.
ExecutorService executor = Executors.newSingleThreadExecutor();
final CountDownLatch taskStarted = new CountDownLatch(1);
final CountDownLatch taskCompleted = new CountDownLatch(1); // <- to sync on task completion
final Exception[] wasSomethingWrong = new Exception[1]; // <- not thread safe, but works here
Future<String> future = executor.submit( new Callable<String>() {
@Override
public synchronized String call() throws Exception {
try {
taskStarted.countDown();
this.wait( 60000 );
}
catch( Exception iWantToGetThisExceptionOutside ) {
wasSomethingWrong[0] = iWantToGetThisExceptionOutside; // <-
} finally {
taskCompleted.countDown(); // <-
}
return "foo";
}
});
assertTrue(taskStarted.await(60, TimeUnit.SECONDS));
try {
future.get(500, TimeUnit.MILLISECONDS);
fail("Timeout expected.");
} catch (ExecutionException | TimeoutException e) {
e.printStackTrace();
}
future.cancel(true); //mayInterruptIfRunning
taskCompleted.await(60, TimeUnit.SECONDS); // <- sync
assertNotNull(wasSomethingWrong[0]);
System.out.println(Arrays.toString(wasSomethingWrong[0].getStackTrace()));
assertEquals(InterruptedException.class, wasSomethingWrong[0].getClass()); // <- PROFIT
source to share