This patch improves the way runAsync calls its callbacks in two
ways.  First, if an exception happens while calling a callback, the
exception is passed to GWT's global uncaught exception handler.
Second, the callbacks happen in the order they are registered.  This
is done by updating the linked-list code that manages the list of
pending runAsync callbacks.

RunAsyncTest is updated to test for both of these.


Review by: kprobst


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@4697 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/FragmentLoaderCreator.java b/dev/core/src/com/google/gwt/dev/jjs/impl/FragmentLoaderCreator.java
index cfb0e54..72310d3 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/FragmentLoaderCreator.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/FragmentLoaderCreator.java
@@ -25,11 +25,11 @@
 import com.google.gwt.dev.cfg.PublicOracle;
 import com.google.gwt.dev.cfg.StaticPropertyOracle;
 import com.google.gwt.dev.javac.CompilationState;
+import com.google.gwt.dev.jdt.FindDeferredBindingSitesVisitor;
 import com.google.gwt.dev.shell.StandardGeneratorContext;
 
 import java.io.File;
 import java.io.PrintWriter;
-import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -45,7 +45,10 @@
   public static final String ASYNC_LOADER_CLASS_PREFIX = "AsyncLoader";
   public static final String ASYNC_LOADER_PACKAGE = "com.google.gwt.lang.asyncloaders";
   public static final String RUN_ASYNC_CALLBACK = "com.google.gwt.core.client.RunAsyncCallback";
+  private static final String GWT_CLASS = FindDeferredBindingSitesVisitor.MAGIC_CLASS;
   private static final String PROP_RUN_ASYNC_NEVER_RUNS = "gwt.jjs.runAsyncNeverRuns";
+  private static final String UNCAUGHT_EXCEPTION_HANDLER_CLASS = GWT_CLASS
+      + ".UncaughtExceptionHandler";
 
   private final ArtifactSet artifactSet;
   private final CompilationState compilationState;
@@ -110,12 +113,23 @@
   }
 
   private void generateLoaderFields(PrintWriter srcWriter) {
+    srcWriter.println("// Whether the code for this entry point has loaded");
     srcWriter.println("private static boolean loaded = false;");
+
+    srcWriter.println("// Whether the code for this entry point is currently loading");
     srcWriter.println("private static boolean loading = false;");
+
+    srcWriter.println("// A callback caller for this entry point");
     srcWriter.println("private static " + getLoaderSuperclassSimpleName()
         + " instance = new " + getLoaderSuperclassSimpleName() + "();");
+
+    srcWriter.println("// Callbacks that are pending");
     srcWriter.println("private static " + getCallbackListSimpleName()
-        + " callbacks = null;");
+        + " callbacksHead = null;");
+
+    srcWriter.println("// The tail of the callbacks list");
+    srcWriter.println("private static " + getCallbackListSimpleName()
+        + " callbacksTail = null;");
   }
 
   private void generateOnErrorMethod(PrintWriter srcWriter) {
@@ -145,11 +159,19 @@
    */
   private void generateRunAsyncMethod(PrintWriter srcWriter) {
     srcWriter.println("public static void runAsync(RunAsyncCallback callback) {");
-    srcWriter.println(getCallbackListSimpleName() + " newCallbackList = new "
+    srcWriter.println(getCallbackListSimpleName() + " newCallback = new "
         + getCallbackListSimpleName() + "();");
-    srcWriter.println("newCallbackList.callback = callback;");
-    srcWriter.println("newCallbackList.next = callbacks;");
-    srcWriter.println("callbacks = newCallbackList;");
+    srcWriter.println("newCallback.callback = callback;");
+
+    srcWriter.println("if (callbacksTail != null) {");
+    srcWriter.println("  callbacksTail.next = newCallback;");
+    srcWriter.println("}");
+
+    srcWriter.println("callbacksTail = newCallback;");
+    srcWriter.println("if (callbacksHead == null) {");
+    srcWriter.println("  callbacksHead = newCallback;");
+    srcWriter.println("}");
+
     srcWriter.println("if (loaded) {");
     srcWriter.println("instance.runCallbacks();");
     srcWriter.println("return;");
@@ -163,29 +185,44 @@
 
   private void generateRunCallbackOnFailuresMethod(PrintWriter srcWriter) {
     srcWriter.println("private static void runCallbackOnFailures(Throwable e) {");
-    // TODO(spoon): this runs the callbacks in reverse order
-    srcWriter.println(getCallbackListSimpleName() + " callback = callbacks;");
-    srcWriter.println("callbacks = null;");
-    srcWriter.println("while (callback != null) {");
-    srcWriter.println("callback.callback.onFailure(e);");
-    srcWriter.println("callback = callback.next;");
+    srcWriter.println("while (callbacksHead != null) {");
+    srcWriter.println("callbacksHead.callback.onFailure(e);");
+    srcWriter.println("callbacksHead = callbacksHead.next;");
     srcWriter.println("}");
+    srcWriter.println("callbacksTail = null;");
     srcWriter.println("}");
   }
 
   private void generateRunCallbacksMethod(PrintWriter srcWriter) {
     srcWriter.println("public void runCallbacks() {");
-    // TODO(spoon): this runs the callbacks in reverse order
-    // TODO(spoon): this runs the callbacks immediately; deferred would be
-    // better
-    srcWriter.println(getCallbackListSimpleName() + " callback = callbacks;");
-    srcWriter.println("callbacks = null;");
+
+    srcWriter.println("while (callbacksHead != null) {");
+
+    srcWriter.println("  " + UNCAUGHT_EXCEPTION_HANDLER_CLASS + " handler = "
+        + FindDeferredBindingSitesVisitor.MAGIC_CLASS
+        + ".getUncaughtExceptionHandler();");
+
+    srcWriter.println("  " + getCallbackListSimpleName() + " next = callbacksHead;");
+    srcWriter.println("  callbacksHead = callbacksHead.next;");
+    srcWriter.println("  if (callbacksHead == null) {");
+    srcWriter.println("    callbacksTail = null;");
+    srcWriter.println("  }");
+
     if (!Boolean.getBoolean(PROP_RUN_ASYNC_NEVER_RUNS)) {
-      srcWriter.println("while (callback != null) {");
-      srcWriter.println("callback.callback.onSuccess();");
-      srcWriter.println("callback = callback.next;");
-      srcWriter.println("}");
+      // TODO(spoon): this runs the callbacks immediately; deferred would be
+      // better
+      srcWriter.println("  if (handler == null) {");
+      srcWriter.println("    next.callback.onSuccess();");
+      srcWriter.println("  } else {");
+      srcWriter.println("    try {");
+      srcWriter.println("      next.callback.onSuccess();");
+      srcWriter.println("    } catch (Throwable e) {");
+      srcWriter.println("      handler.onUncaughtException(e);");
+      srcWriter.println("    }");
+      srcWriter.println("  }");
     }
+
+    srcWriter.println("}");
     srcWriter.println("}");
   }
 
@@ -228,7 +265,7 @@
     printWriter.println("package " + getPackage() + ";");
     String[] imports = new String[] {
         RUN_ASYNC_CALLBACK, List.class.getCanonicalName(),
-        ArrayList.class.getCanonicalName(), ASYNC_FRAGMENT_LOADER};
+        ASYNC_FRAGMENT_LOADER};
     for (String imp : imports) {
       printWriter.println("import " + imp + ";");
     }
@@ -271,10 +308,10 @@
 
   /**
    * Create a stand-in superclass of the actual loader. This is used to keep the
-   * liveness analyzer from thinking the real <code>runCallbacks()</code>
-   * method is available until <code>onLoad</code> has been called and the
-   * real loader instantiated. A little work on TypeTightener could prevent the
-   * need for this class.
+   * liveness analyzer from thinking the real <code>runCallbacks()</code> method
+   * is available until <code>onLoad</code> has been called and the real loader
+   * instantiated. A little work on TypeTightener could prevent the need for
+   * this class.
    */
   private void writeLoaderSuperclass(TreeLogger logger, GeneratorContext ctx)
       throws UnableToCompleteException {
diff --git a/user/src/com/google/gwt/core/client/GWT.java b/user/src/com/google/gwt/core/client/GWT.java
index 1184d9e..6062bd1 100644
--- a/user/src/com/google/gwt/core/client/GWT.java
+++ b/user/src/com/google/gwt/core/client/GWT.java
@@ -188,10 +188,20 @@
    */
   public static void runAsync(RunAsyncCallback callback) {
     /*
-     * By default, just call the callback. This allows using <code>runAsync</code>
-     * in code that might or might not run in a web browser.
+     * By default, just call the callback. This allows using
+     * <code>runAsync</code> in code that might or might not run in a web
+     * browser.
      */
-    callback.onSuccess();
+    UncaughtExceptionHandler handler = sUncaughtExceptionHandler;
+    if (handler == null) {
+      callback.onSuccess();
+    } else {
+      try {
+        callback.onSuccess();
+      } catch (Throwable e) {
+        handler.onUncaughtException(e);
+      }
+    }
   }
 
   /**
diff --git a/user/test/com/google/gwt/dev/jjs/test/RunAsyncTest.java b/user/test/com/google/gwt/dev/jjs/test/RunAsyncTest.java
index ad4a702..e3a49e0 100644
--- a/user/test/com/google/gwt/dev/jjs/test/RunAsyncTest.java
+++ b/user/test/com/google/gwt/dev/jjs/test/RunAsyncTest.java
@@ -17,23 +17,24 @@
 
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.RunAsyncCallback;
+import com.google.gwt.core.client.GWT.UncaughtExceptionHandler;
 import com.google.gwt.junit.client.GWTTestCase;
 
 /**
  * Tests runAsync in various ways.
  */
 public class RunAsyncTest extends GWTTestCase {
+  private static final String HELLO = "hello";
+
   private static final int RUNASYNC_TIMEOUT = 10000;
 
+  private static String staticWrittenInBaseButReadLater;
+
   @Override
   public String getModuleName() {
     return "com.google.gwt.dev.jjs.CompilerSuite";
   }
 
-  private static final String HELLO = "hello";
-
-  private static String staticWrittenInBaseButReadLater;
-
   public void testBasic() {
     delayTestFinish(RUNASYNC_TIMEOUT);
 
@@ -71,4 +72,74 @@
     });
   }
 
+  /**
+   * Test that callbacks are called in the order they are posted.
+   */
+  public void testOrder() {
+    class Util {
+      int callNumber = 0;
+      int seen = 0;
+
+      void scheduleCallback() {
+        final int thisCallNumber = callNumber++;
+
+        GWT.runAsync(new RunAsyncCallback() {
+
+          public void onFailure(Throwable caught) {
+            throw new RuntimeException(caught);
+          }
+
+          public void onSuccess() {
+            assertEquals(seen, thisCallNumber);
+            seen++;
+            if (seen == 3) {
+              finishTest();
+            }
+          }
+        });
+      }
+    }
+    Util util = new Util();
+
+    delayTestFinish(RUNASYNC_TIMEOUT);
+
+    util.scheduleCallback();
+    util.scheduleCallback();
+    util.scheduleCallback();
+  }
+
+  public void testUnhandledExceptions() {
+    // Create an exception that will be thrown from an onSuccess method
+    final RuntimeException toThrow = new RuntimeException(
+        "Should be caught by the uncaught exception handler");
+
+    // save the original handler
+    final UncaughtExceptionHandler originalHandler = GWT.getUncaughtExceptionHandler();
+
+    // set a handler that catches toThrow and nothing else
+    GWT.setUncaughtExceptionHandler(new GWT.UncaughtExceptionHandler() {
+      public void onUncaughtException(Throwable e) {
+        GWT.setUncaughtExceptionHandler(originalHandler);
+
+        if (e == toThrow) {
+          // expected
+          finishTest();
+        } else {
+          // some other exception; pass it on
+          throw new RuntimeException(e);
+        }
+      }
+    });
+
+    delayTestFinish(RUNASYNC_TIMEOUT);
+
+    GWT.runAsync(new RunAsyncCallback() {
+      public void onFailure(Throwable caught) {
+      }
+
+      public void onSuccess() {
+        throw toThrow;
+      }
+    });
+  }
 }