From 7142fb496ae20f495a771daffa6a87b1d98a07f6 Mon Sep 17 00:00:00 2001 From: Thomas Bruyelle Date: Fri, 9 Aug 2013 11:56:32 +0200 Subject: [PATCH] Propagate exceptions for submitted/scheduled tasks --- .../api/BackgroundExecutor.java | 37 ++++++++++++++++++- .../test15/ThreadActivityTest.java | 37 +++++++++++++++++++ .../test15/ThreadActivity.java | 6 ++- 3 files changed, 78 insertions(+), 2 deletions(-) diff --git a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/api/BackgroundExecutor.java b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/api/BackgroundExecutor.java index 450708c701..6d2b5edaae 100644 --- a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/api/BackgroundExecutor.java +++ b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/api/BackgroundExecutor.java @@ -17,6 +17,8 @@ import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -32,6 +34,9 @@ public class BackgroundExecutor { private static final String TAG = "BackgroundExecutor"; public static Executor DEFAULT_EXECUTOR = Executors.newScheduledThreadPool(2 * Runtime.getRuntime().availableProcessors()); + + public static Executor exceptionWatcherExecutor = Executors.newFixedThreadPool(2 * Runtime.getRuntime().availableProcessors()); + private static Executor executor = DEFAULT_EXECUTOR; private static final List tasks = new ArrayList(); @@ -51,7 +56,7 @@ public class BackgroundExecutor { * @return Future associated to the running task */ private static Future directExecute(Runnable runnable, int delay) { - Future future = null; + final Future future; if (delay > 0) { /* no serial, but a delay: schedule the task */ if (!(executor instanceof ScheduledExecutorService)) { @@ -66,8 +71,38 @@ private static Future directExecute(Runnable runnable, int delay) { } else { /* non-cancellable task */ executor.execute(runnable); + future = null; } } + + /* + * Because ExecutorService.submit/schedule doesn't propagate exception + * during executions, we need to call futur.get() (in another thread + * pool) to check if an exception occured during execution. If yes then + * throw it so it could be caught by the system. + */ + if (future != null) { + exceptionWatcherExecutor.execute(new Runnable() { + @Override + public void run() { + try { + future.get(); + } catch (InterruptedException e) { + Log.w(TAG, "Scheduled execution was interrupted", e); + } catch (CancellationException e) { + Log.w(TAG, "Watcher thread has been cancelled", e); + } catch (ExecutionException e) { + if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } + if (e.getCause() instanceof Error) { + throw (Error) e.getCause(); + } + } + } + }); + } + return future; } diff --git a/AndroidAnnotations/functional-test-1-5-tests/src/test/java/org/androidannotations/test15/ThreadActivityTest.java b/AndroidAnnotations/functional-test-1-5-tests/src/test/java/org/androidannotations/test15/ThreadActivityTest.java index 8ae00b7729..e53878485c 100644 --- a/AndroidAnnotations/functional-test-1-5-tests/src/test/java/org/androidannotations/test15/ThreadActivityTest.java +++ b/AndroidAnnotations/functional-test-1-5-tests/src/test/java/org/androidannotations/test15/ThreadActivityTest.java @@ -25,6 +25,7 @@ import java.util.Random; import java.util.concurrent.Executor; import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; @@ -285,4 +286,40 @@ public void execute(Runnable command) { } } + /** + * Verify that exceptions thrown in threads from executorService are visible + * to exception handlers. + * + * Set a custom exception handler which stores the throwable and then check + * its content. + */ + @Test + public void propagateExceptionWithExecutorService() throws InterruptedException { + final Thread.UncaughtExceptionHandler originExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); + + final TestExceptionHandler testExceptionHandler = new TestExceptionHandler(); + Thread.setDefaultUncaughtExceptionHandler(testExceptionHandler); + + BackgroundExecutor.setExecutor(new ScheduledThreadPoolExecutor(1)); + + activity.backgroundThrowException(); + Thread.sleep(500); + Assert.assertNotNull("Exception should be propagated in @Backgound annotated methods", testExceptionHandler.throwable); + + testExceptionHandler.throwable = null; + + activity.backgroundDelayThrowException(); + Thread.sleep(500); + Assert.assertNotNull("Exception should be propagated in @Backgound(delay) annotated methods", testExceptionHandler.throwable); + Thread.setDefaultUncaughtExceptionHandler(originExceptionHandler); + } + + class TestExceptionHandler implements Thread.UncaughtExceptionHandler { + Throwable throwable; + + @Override + public void uncaughtException(Thread thread, Throwable ex) { + throwable = ex; + } + } } diff --git a/AndroidAnnotations/functional-test-1-5/src/main/java/org/androidannotations/test15/ThreadActivity.java b/AndroidAnnotations/functional-test-1-5/src/main/java/org/androidannotations/test15/ThreadActivity.java index 5b0e66237a..804242bd8a 100644 --- a/AndroidAnnotations/functional-test-1-5/src/main/java/org/androidannotations/test15/ThreadActivity.java +++ b/AndroidAnnotations/functional-test-1-5/src/main/java/org/androidannotations/test15/ThreadActivity.java @@ -29,7 +29,6 @@ import org.androidannotations.test15.instancestate.MySerializableBean; import android.app.Activity; -import android.os.Looper; @EActivity public class ThreadActivity extends Activity { @@ -145,6 +144,11 @@ void backgroundThrowException() { throw new RuntimeException(); } + @Background(delay = 100) + void backgroundDelayThrowException() { + throw new RuntimeException(); + } + @UiThread void uiThreadThrowException() { throw new RuntimeException();