]> source.dussan.org Git - vaadin-framework.git/commitdiff
Make access() enqueue the runnable if the session is locked (#11897)
authorLeif Åstrand <leif@vaadin.com>
Wed, 22 May 2013 12:56:29 +0000 (15:56 +0300)
committerVaadin Code Review <review@vaadin.com>
Tue, 28 May 2013 07:47:48 +0000 (07:47 +0000)
Change-Id: If162e81a29bbc982857e2a165a983e161ea837ee

server/src/com/vaadin/server/RequestHandler.java
server/src/com/vaadin/server/VaadinService.java
server/src/com/vaadin/server/VaadinSession.java
server/src/com/vaadin/server/communication/FileUploadHandler.java
server/src/com/vaadin/server/communication/UidlWriter.java
server/src/com/vaadin/ui/LoginForm.java
server/src/com/vaadin/ui/UI.java
uitest/src/com/vaadin/tests/applicationcontext/CloseUI.java
uitest/src/com/vaadin/tests/applicationcontext/UIRunSafelyThread.java
uitest/src/com/vaadin/tests/components/ui/UiAccess.html [new file with mode: 0644]
uitest/src/com/vaadin/tests/components/ui/UiAccess.java [new file with mode: 0644]

index 873752c5f2ae8bcc7dc65d55f426b4c54f33af70..097a3e034b40bf475e0dee51774747be14f0e917 100644 (file)
@@ -37,7 +37,8 @@ public interface RequestHandler extends Serializable {
      * using VaadinSession or anything inside the VaadinSession you must ensure
      * the session is locked. This can be done by extending
      * {@link SynchronizedRequestHandler} or by using
-     * {@link VaadinSession#access(Runnable)} or {@link UI#access(Runnable)}.
+     * {@link VaadinSession#accessSynchronously(Runnable)} or
+     * {@link UI#accessSynchronously(Runnable)}.
      * </p>
      * 
      * @param session
index af0c280c19fde01dc176f56ecdd6f9d836909fc8..2cb7f9059eee51eab27e981619d0e3d9dadd8823 100644 (file)
@@ -407,12 +407,12 @@ public abstract class VaadinService implements Serializable {
      */
     public void fireSessionDestroy(VaadinSession vaadinSession) {
         final VaadinSession session = vaadinSession;
-        session.access(new Runnable() {
+        session.accessSynchronously(new Runnable() {
             @Override
             public void run() {
                 ArrayList<UI> uis = new ArrayList<UI>(session.getUIs());
                 for (final UI ui : uis) {
-                    ui.access(new Runnable() {
+                    ui.accessSynchronously(new Runnable() {
                         @Override
                         public void run() {
                             /*
@@ -1087,7 +1087,7 @@ public abstract class VaadinService implements Serializable {
     private void removeClosedUIs(final VaadinSession session) {
         ArrayList<UI> uis = new ArrayList<UI>(session.getUIs());
         for (final UI ui : uis) {
-            ui.access(new Runnable() {
+            ui.accessSynchronously(new Runnable() {
                 @Override
                 public void run() {
                     if (ui.isClosing()) {
@@ -1245,7 +1245,7 @@ public abstract class VaadinService implements Serializable {
         if (session != null) {
             final VaadinSession finalSession = session;
 
-            session.access(new Runnable() {
+            session.accessSynchronously(new Runnable() {
                 @Override
                 public void run() {
                     cleanupSession(finalSession);
@@ -1254,7 +1254,7 @@ public abstract class VaadinService implements Serializable {
 
             final long duration = (System.nanoTime() - (Long) request
                     .getAttribute(REQUEST_START_TIME_ATTRIBUTE)) / 1000000;
-            session.access(new Runnable() {
+            session.accessSynchronously(new Runnable() {
                 @Override
                 public void run() {
                     finalSession.setLastRequestDuration(duration);
@@ -1542,8 +1542,9 @@ public abstract class VaadinService implements Serializable {
 
     /**
      * Checks that another {@link VaadinSession} instance is not locked. This is
-     * internally used by {@link VaadinSession#access(Runnable)} and
-     * {@link UI#access(Runnable)} to help avoid causing deadlocks.
+     * internally used by {@link VaadinSession#accessSynchronously(Runnable)}
+     * and {@link UI#accessSynchronously(Runnable)} to help avoid causing
+     * deadlocks.
      * 
      * @since 7.1
      * @param session
index 317ea6cf7b31b4a6670c46c65602b67e6f0d799d..9625a3f3504af6420d9dbbe3b0d60becefdfe043 100644 (file)
@@ -26,6 +26,11 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.UUID;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 import java.util.logging.Logger;
@@ -130,6 +135,13 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable {
 
     private transient Lock lock;
 
+    /*
+     * Pending tasks can't be serialized and the queue should be empty when the
+     * session is serialized as long as it doesn't happen while some other
+     * thread has the lock.
+     */
+    private transient final ConcurrentLinkedQueue<FutureTask<Void>> pendingAccessQueue = new ConcurrentLinkedQueue<FutureTask<Void>>();
+
     /**
      * Create a new service session tied to a Vaadin service
      * 
@@ -820,9 +832,13 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable {
     public void unlock() {
         assert hasLock();
         try {
+            /*
+             * Run pending tasks and push if the reentrant lock will actually be
+             * released by this unlock() invocation.
+             */
             if (((ReentrantLock) getLockInstance()).getHoldCount() == 1) {
-                // Only push if the reentrant lock will actually be released by
-                // this unlock() invocation.
+                runPendingAccessTasks();
+
                 for (UI ui : getUIs()) {
                     if (ui.getPushMode() == PushMode.AUTOMATIC) {
                         ui.push();
@@ -1063,23 +1079,30 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable {
     }
 
     /**
-     * Provides exclusive access to this session from outside a request handling
-     * thread.
+     * Locks this session and runs the provided Runnable right away.
      * <p>
-     * The given runnable is executed while holding the session lock to ensure
-     * exclusive access to this session. The session and related thread locals
-     * are set properly before executing the runnable.
+     * It is generally recommended to use {@link #access(Runnable)} instead of
+     * this method for accessing a session from a different thread as
+     * {@link #access(Runnable)} can be used while holding the lock of another
+     * session. To avoid causing deadlocks, this methods throws an exception if
+     * it is detected than another session is also locked by the current thread.
      * </p>
      * <p>
-     * RPC handlers for components inside this session do not need this method
-     * as the session is automatically locked by the framework during request
-     * handling.
-     * </p>
-     * <p>
-     * Note that calling this method while another session is locked by the
-     * current thread will cause an exception. This is to prevent deadlock
-     * situations when two threads have locked one session each and are both
-     * waiting for the lock for the other session.
+     * This method behaves differently than {@link #access(Runnable)} in some
+     * situations:
+     * <ul>
+     * <li>If the current thread is currently holding the lock of this session,
+     * {@link #accessSynchronously(Runnable)} runs the task right away whereas
+     * {@link #access(Runnable)} defers the task to a later point in time.</li>
+     * <li>If some other thread is currently holding the lock for this session,
+     * {@link #accessSynchronously(Runnable)} blocks while waiting for the lock
+     * to be available whereas {@link #access(Runnable)} defers the task to a
+     * later point in time.</li>
+     * <li>If this session is currently not locked,
+     * {@link #accessSynchronously(Runnable)} runs the task right away whereas
+     * {@link #access(Runnable)} defers the task to a later point in time unless
+     * there are UIs with automatic push enabled.</li>
+     * </ul>
      * </p>
      * 
      * @param runnable
@@ -1088,12 +1111,14 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable {
      * @throws IllegalStateException
      *             if the current thread holds the lock for another session
      * 
+     * @since 7.1
      * 
      * @see #lock()
      * @see #getCurrent()
-     * @see UI#access(Runnable)
+     * @see #access(Runnable)
+     * @see UI#accessSynchronously(Runnable)
      */
-    public void access(Runnable runnable) {
+    public void accessSynchronously(Runnable runnable) {
         VaadinService.verifyNoOtherSessionLocked(this);
 
         Map<Class<?>, CurrentInstance> old = null;
@@ -1111,12 +1136,119 @@ public class VaadinSession implements HttpSessionBindingListener, Serializable {
     }
 
     /**
-     * @deprecated As of 7.1.0.beta1, use {@link #access(Runnable)} instead.
-     *             This method will be removed before the final 7.1.0 release.
+     * Provides exclusive access to this session from outside a request handling
+     * thread.
+     * <p>
+     * The given runnable is executed while holding the session lock to ensure
+     * exclusive access to this session. If this session is not locked, the lock
+     * will be acquired and the runnable is run right away. If this session is
+     * currently locked, the runnable will be run before that lock is released.
+     * </p>
+     * <p>
+     * RPC handlers for components inside this session do not need to use this
+     * method as the session is automatically locked by the framework during RPC
+     * handling.
+     * </p>
+     * <p>
+     * Please note that the runnable might be invoked on a different thread or
+     * later on the current thread, which means that custom thread locals might
+     * not have the expected values when the runnable is executed. The session
+     * and other thread locals provided by Vaadin are set properly before
+     * executing the runnable.
+     * </p>
+     * <p>
+     * The returned future can be used to check for task completion and to
+     * cancel the task. To help avoiding deadlocks, {@link Future#get()} throws
+     * an exception if it is detected that the current thread holds the lock for
+     * some other session.
+     * </p>
+     * 
+     * @see #lock()
+     * @see #getCurrent()
+     * @see #accessSynchronously(Runnable)
+     * @see UI#access(Runnable)
+     * 
+     * @since 7.1
+     * 
+     * @param runnable
+     *            the runnable which accesses the session
+     * @return a future that can be used to check for task completion and to
+     *         cancel the task
+     */
+    public Future<Void> access(Runnable runnable) {
+        FutureTask<Void> future = new FutureTask<Void>(runnable, null) {
+            @Override
+            public Void get() throws InterruptedException, ExecutionException {
+                /*
+                 * Help the developer avoid programming patterns that cause
+                 * deadlocks unless implemented very carefully. get(long,
+                 * TimeUnit) does not have the same detection since a sensible
+                 * timeout should avoid completely locking up the application.
+                 * 
+                 * Even though no deadlock could occur after the runnable has
+                 * been run, the check is always done as the deterministic
+                 * behavior makes it easier to detect potential problems.
+                 */
+                VaadinService.verifyNoOtherSessionLocked(VaadinSession.this);
+                return super.get();
+            }
+        };
+        pendingAccessQueue.add(future);
+
+        /*
+         * If no thread is currently holding the lock, pending changes for UIs
+         * with automatic push would not be processed and pushed until the next
+         * time there is a request or someone does an explicit push call.
+         * 
+         * To remedy this, we try to get the lock at this point. If the lock is
+         * currently held by another thread, we just back out as the queue will
+         * get purged once it is released. If the lock is held by the current
+         * thread, we just release it knowing that the queue gets purged once
+         * the lock is ultimately released. If the lock is not held by any
+         * thread and we acquire it, we just release it again to purge the queue
+         * right away.
+         */
+        try {
+            // tryLock() would be shorter, but it does not guarantee fairness
+            if (getLockInstance().tryLock(0, TimeUnit.SECONDS)) {
+                // unlock triggers runPendingAccessTasks
+                unlock();
+            }
+        } catch (InterruptedException e) {
+            // Just ignore
+        }
+
+        return future;
+    }
+
+    /**
+     * Purges the queue of pending access invocations enqueued with
+     * {@link #access(Runnable)}.
+     * <p>
+     * This method is automatically run by the framework at appropriate
+     * situations and is not intended to be used by application developers.
+     * 
+     * @since 7.1
+     */
+    public void runPendingAccessTasks() {
+        assert hasLock();
+
+        FutureTask<Void> pendingAccess;
+        while ((pendingAccess = pendingAccessQueue.poll()) != null) {
+            if (!pendingAccess.isCancelled()) {
+                accessSynchronously(pendingAccess);
+            }
+        }
+    }
+
+    /**
+     * @deprecated As of 7.1.0.beta1, use {@link #accessSynchronously(Runnable)}
+     *             or {@link #access(Runnable)} instead. This method will be
+     *             removed before the final 7.1.0 release.
      */
     @Deprecated
     public void runSafely(Runnable runnable) {
-        access(runnable);
+        accessSynchronously(runnable);
     }
 
     /**
index e875a4e8618f9261b27b68912c018fbc4a7d5c36..e9569d45a171fa4b5131c0acf7dcf658e500b568 100644 (file)
@@ -632,7 +632,7 @@ public class FileUploadHandler implements RequestHandler {
 
     private void cleanStreamVariable(VaadinSession session,
             final ClientConnector owner, final String variableName) {
-        session.access(new Runnable() {
+        session.accessSynchronously(new Runnable() {
             @Override
             public void run() {
                 owner.getUI()
index fbe2fb86d59a25938f1a03454a4c864e868fad8b..60a884a63534a686b486320c522438d02d76b30e 100644 (file)
@@ -74,9 +74,14 @@ public class UidlWriter implements Serializable {
     public void write(UI ui, Writer writer, boolean repaintAll,
             boolean analyzeLayouts, boolean async) throws IOException,
             JSONException {
+        VaadinSession session = ui.getSession();
+
+        // Purge pending access calls as they might produce additional changes
+        // to write out
+        session.runPendingAccessTasks();
+
         ArrayList<ClientConnector> dirtyVisibleConnectors = ui
                 .getConnectorTracker().getDirtyVisibleConnectors();
-        VaadinSession session = ui.getSession();
         LegacyCommunicationManager manager = session.getCommunicationManager();
         // Paints components
         ConnectorTracker uiConnectorTracker = ui.getConnectorTracker();
index d06882927ee8067a0c29c78f41c2b2ca666de319..67d7182ecbd5abafceb79236aa4038e309762350 100644 (file)
@@ -68,7 +68,7 @@ public class LoginForm extends CustomComponent {
         }
         final StringBuilder responseBuilder = new StringBuilder();
 
-        getUI().access(new Runnable() {
+        getUI().accessSynchronously(new Runnable() {
             @Override
             public void run() {
                 String method = VaadinServletService.getCurrentServletRequest()
index e077b003b8537921ffbe84aa170dd20364fce5d2..234a309c061b3ad1880da128466ed44c70e57042 100644 (file)
@@ -22,6 +22,7 @@ import java.util.Collections;
 import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.Map;
+import java.util.concurrent.Future;
 
 import com.vaadin.event.Action;
 import com.vaadin.event.Action.Handler;
@@ -86,7 +87,7 @@ public abstract class UI extends AbstractSingleComponentContainer implements
     /**
      * The application to which this UI belongs
      */
-    private VaadinSession session;
+    private volatile VaadinSession session;
 
     /**
      * List of windows in this UI.
@@ -1098,24 +1099,34 @@ public abstract class UI extends AbstractSingleComponentContainer implements
     }
 
     /**
-     * Provides exclusive access to this UI from outside a request handling
-     * thread.
+     * Locks the session of this UI and runs the provided Runnable right away.
      * <p>
-     * The given runnable is executed while holding the session lock to ensure
-     * exclusive access to this UI and its session. The UI and related thread
-     * locals are set properly before executing the runnable.
+     * It is generally recommended to use {@link #access(Runnable)} instead of
+     * this method for accessing a session from a different thread as
+     * {@link #access(Runnable)} can be used while holding the lock of another
+     * session. To avoid causing deadlocks, this methods throws an exception if
+     * it is detected than another session is also locked by the current thread.
      * </p>
      * <p>
-     * RPC handlers for components inside this UI do not need this method as the
-     * session is automatically locked by the framework during request handling.
-     * </p>
-     * <p>
-     * Note that calling this method while another session is locked by the
-     * current thread will cause an exception. This is to prevent deadlock
-     * situations when two threads have locked one session each and are both
-     * waiting for the lock for the other session.
+     * This method behaves differently than {@link #access(Runnable)} in some
+     * situations:
+     * <ul>
+     * <li>If the current thread is currently holding the lock of the session,
+     * {@link #accessSynchronously(Runnable)} runs the task right away whereas
+     * {@link #access(Runnable)} defers the task to a later point in time.</li>
+     * <li>If some other thread is currently holding the lock for the session,
+     * {@link #accessSynchronously(Runnable)} blocks while waiting for the lock
+     * to be available whereas {@link #access(Runnable)} defers the task to a
+     * later point in time.</li>
+     * <li>If the session is currently not locked,
+     * {@link #accessSynchronously(Runnable)} runs the task right away whereas
+     * {@link #access(Runnable)} defers the task to a later point in time unless
+     * there are UIs with automatic push enabled.</li>
+     * </ul>
      * </p>
      * 
+     * @since 7.1
+     * 
      * @param runnable
      *            the runnable which accesses the UI
      * @throws UIDetachedException
@@ -1124,11 +1135,11 @@ public abstract class UI extends AbstractSingleComponentContainer implements
      * @throws IllegalStateException
      *             if the current thread holds the lock for another session
      * 
-     * @see #getCurrent()
-     * @see VaadinSession#access(Runnable)
-     * @see VaadinSession#lock()
+     * @see #access(Runnable)
+     * @see VaadinSession#accessSynchronously(Runnable)
      */
-    public void access(Runnable runnable) throws UIDetachedException {
+    public void accessSynchronously(Runnable runnable)
+            throws UIDetachedException {
         Map<Class<?>, CurrentInstance> old = null;
 
         VaadinSession session = getSession();
@@ -1158,12 +1169,69 @@ public abstract class UI extends AbstractSingleComponentContainer implements
     }
 
     /**
-     * @deprecated As of 7.1.0.beta1, use {@link #access(Runnable)} instead.
-     *             This method will be removed before the final 7.1.0 release.
+     * Provides exclusive access to this UI from outside a request handling
+     * thread.
+     * <p>
+     * The given runnable is executed while holding the session lock to ensure
+     * exclusive access to this UI. If the session is not locked, the lock will
+     * be acquired and the runnable is run right away. If the session is
+     * currently locked, the runnable will be run before that lock is released.
+     * </p>
+     * <p>
+     * RPC handlers for components inside this UI do not need to use this method
+     * as the session is automatically locked by the framework during RPC
+     * handling.
+     * </p>
+     * <p>
+     * Please note that the runnable might be invoked on a different thread or
+     * later on the current thread, which means that custom thread locals might
+     * not have the expected values when the runnable is executed. The UI and
+     * other thread locals provided by Vaadin are set properly before executing
+     * the runnable.
+     * </p>
+     * <p>
+     * The returned future can be used to check for task completion and to
+     * cancel the task.
+     * </p>
+     * 
+     * @see #getCurrent()
+     * @see #accessSynchronously(Runnable)
+     * @see VaadinSession#access(Runnable)
+     * @see VaadinSession#lock()
+     * 
+     * @since 7.1
+     * 
+     * @param runnable
+     *            the runnable which accesses the UI
+     * @throws UIDetachedException
+     *             if the UI is not attached to a session (and locking can
+     *             therefore not be done)
+     * @return a future that can be used to check for task completion and to
+     *         cancel the task
+     */
+    public Future<Void> access(final Runnable runnable) {
+        VaadinSession session = getSession();
+
+        if (session == null) {
+            throw new UIDetachedException();
+        }
+
+        return session.access(new Runnable() {
+            @Override
+            public void run() {
+                accessSynchronously(runnable);
+            }
+        });
+    }
+
+    /**
+     * @deprecated As of 7.1.0.beta1, use {@link #accessSynchronously(Runnable)}
+     *             or {@link #access(Runnable)} instead. This method will be
+     *             removed before the final 7.1.0 release.
      */
     @Deprecated
     public void runSafely(Runnable runnable) throws UIDetachedException {
-        access(runnable);
+        accessSynchronously(runnable);
     }
 
     /**
@@ -1204,6 +1272,14 @@ public abstract class UI extends AbstractSingleComponentContainer implements
         VaadinSession session = getSession();
         if (session != null) {
             assert session.hasLock();
+
+            /*
+             * Purge the pending access queue as it might mark a connector as
+             * dirty when the push would otherwise be ignored because there are
+             * no changes to push.
+             */
+            session.runPendingAccessTasks();
+
             if (!getConnectorTracker().hasDirtyConnectors()) {
                 // Do not push if there is nothing to push
                 return;
index bec8c0a10fa5ae2ae0553d0dcb1dc2f708c52f14..c88f482a7b08b07124b579be076d25aea2899ddb 100644 (file)
@@ -119,7 +119,7 @@ public class CloseUI extends AbstractTestUI {
 
         @Override
         public void run() {
-            ui.access(new Runnable() {
+            ui.accessSynchronously(new Runnable() {
 
                 @Override
                 public void run() {
index ddc0f28664780db3f0783accc8ebec30f4be5389..c9af2c000d5f74e25cd3d86196b19ca1eb2e1664 100644 (file)
@@ -11,7 +11,7 @@ public abstract class UIRunSafelyThread extends Thread {
 
     @Override
     public void run() {
-        ui.access(new Runnable() {
+        ui.accessSynchronously(new Runnable() {
 
             @Override
             public void run() {
diff --git a/uitest/src/com/vaadin/tests/components/ui/UiAccess.html b/uitest/src/com/vaadin/tests/components/ui/UiAccess.html
new file mode 100644 (file)
index 0000000..664b15c
--- /dev/null
@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+<link rel="selenium.base" href="" />
+<title>New Test</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr><td rowspan="1" colspan="3">New Test</td></tr>
+</thead><tbody>
+<tr>
+       <td>open</td>
+       <td>/run/com.vaadin.tests.components.ui.UiAccess?restartApplication</td>
+       <td></td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>vaadin=runcomvaadintestscomponentsuiUiAccess::/VVerticalLayout[0]/Slot[2]/VVerticalLayout[0]/Slot[0]/VButton[0]/domChild[0]/domChild[0]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>vaadin=runcomvaadintestscomponentsuiUiAccess::PID_SLog_row_2</td>
+       <td>exact:0. Access from UI thread future is done? false</td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>vaadin=runcomvaadintestscomponentsuiUiAccess::PID_SLog_row_1</td>
+       <td>1. Access from UI thread is run</td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>vaadin=runcomvaadintestscomponentsuiUiAccess::PID_SLog_row_0</td>
+       <td>exact:2. beforeClientResponse future is done? true</td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>vaadin=runcomvaadintestscomponentsuiUiAccess::/VVerticalLayout[0]/Slot[2]/VVerticalLayout[0]/Slot[1]/VButton[0]/domChild[0]/domChild[0]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>vaadin=runcomvaadintestscomponentsuiUiAccess::PID_SLog_row_1</td>
+       <td>0. Initial background message</td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>vaadin=runcomvaadintestscomponentsuiUiAccess::PID_SLog_row_0</td>
+       <td>exact:1. Thread has current response? false</td>
+</tr>
+<tr>
+       <td>waitForText</td>
+       <td>vaadin=runcomvaadintestscomponentsuiUiAccess::PID_SLog_row_4</td>
+       <td>0. Initial background message</td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>vaadin=runcomvaadintestscomponentsuiUiAccess::PID_SLog_row_2</td>
+       <td>exact:2. Thread got lock, inital future done? true</td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>vaadin=runcomvaadintestscomponentsuiUiAccess::PID_SLog_row_1</td>
+       <td>exact:3. Access has current response? true</td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>vaadin=runcomvaadintestscomponentsuiUiAccess::PID_SLog_row_0</td>
+       <td>exact:4. Thread is still alive? false</td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>vaadin=runcomvaadintestscomponentsuiUiAccess::/VVerticalLayout[0]/Slot[2]/VVerticalLayout[0]/Slot[2]/VButton[0]/domChild[0]/domChild[0]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>vaadin=runcomvaadintestscomponentsuiUiAccess::PID_SLog_row_2</td>
+       <td>0. Throwing exception in access</td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>vaadin=runcomvaadintestscomponentsuiUiAccess::PID_SLog_row_1</td>
+       <td>exact:1. firstFuture is done? true</td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>vaadin=runcomvaadintestscomponentsuiUiAccess::PID_SLog_row_0</td>
+       <td>2. Got exception from firstFuture: java.lang.RuntimeException: Catch me if you can</td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>vaadin=runcomvaadintestscomponentsuiUiAccess::/VVerticalLayout[0]/Slot[2]/VVerticalLayout[0]/Slot[3]/VButton[0]/domChild[0]/domChild[0]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>vaadin=runcomvaadintestscomponentsuiUiAccess::PID_SLog_row_0</td>
+       <td>0. future was cancled, should not start</td>
+</tr>
+<tr>
+       <td>click</td>
+       <td>vaadin=runcomvaadintestscomponentsuiUiAccess::/VVerticalLayout[0]/Slot[2]/VVerticalLayout[0]/Slot[4]/VButton[0]/domChild[0]/domChild[0]</td>
+       <td></td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>vaadin=runcomvaadintestscomponentsuiUiAccess::PID_SLog_row_2</td>
+       <td>0. Waiting for thread to start</td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>vaadin=runcomvaadintestscomponentsuiUiAccess::PID_SLog_row_1</td>
+       <td>1. Thread started, waiting for interruption</td>
+</tr>
+<tr>
+       <td>assertText</td>
+       <td>vaadin=runcomvaadintestscomponentsuiUiAccess::PID_SLog_row_0</td>
+       <td>2. I was interrupted</td>
+</tr>
+
+</tbody></table>
+</body>
+</html>
diff --git a/uitest/src/com/vaadin/tests/components/ui/UiAccess.java b/uitest/src/com/vaadin/tests/components/ui/UiAccess.java
new file mode 100644 (file)
index 0000000..c68da6e
--- /dev/null
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.tests.components.ui;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.locks.ReentrantLock;
+
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.server.VaadinService;
+import com.vaadin.tests.components.AbstractTestUIWithLog;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Button.ClickEvent;
+
+public class UiAccess extends AbstractTestUIWithLog {
+
+    private Future<Void> checkFromBeforeClientResponse;
+
+    @Override
+    protected void setup(VaadinRequest request) {
+        addComponent(new Button("Access from UI thread",
+                new Button.ClickListener() {
+
+                    @Override
+                    public void buttonClick(ClickEvent event) {
+                        log.clear();
+                        // Ensure beforeClientResponse is invoked
+                        markAsDirty();
+                        checkFromBeforeClientResponse = access(new Runnable() {
+                            @Override
+                            public void run() {
+                                log("Access from UI thread is run");
+                            }
+                        });
+                        log("Access from UI thread future is done? "
+                                + checkFromBeforeClientResponse.isDone());
+                    }
+                }));
+        addComponent(new Button("Access from background thread",
+                new Button.ClickListener() {
+                    @Override
+                    public void buttonClick(ClickEvent event) {
+                        log.clear();
+                        final CountDownLatch latch = new CountDownLatch(1);
+
+                        new Thread() {
+                            @Override
+                            public void run() {
+                                final boolean threadHasCurrentResponse = VaadinService
+                                        .getCurrentResponse() != null;
+                                // session is locked by request thread at this
+                                // point
+                                final Future<Void> initialFuture = access(new Runnable() {
+                                    @Override
+                                    public void run() {
+                                        log("Initial background message");
+                                        log("Thread has current response? "
+                                                + threadHasCurrentResponse);
+                                    }
+                                });
+
+                                // Let request thread continue
+                                latch.countDown();
+
+                                // Wait until thread can be locked
+                                while (!getSession().getLockInstance()
+                                        .tryLock()) {
+                                    try {
+                                        Thread.sleep(100);
+                                    } catch (InterruptedException e) {
+                                        throw new RuntimeException(e);
+                                    }
+                                }
+                                try {
+                                    log("Thread got lock, inital future done? "
+                                            + initialFuture.isDone());
+                                    setPollInterval(-1);
+                                } finally {
+                                    getSession().unlock();
+                                }
+                                final Thread thisThread = this;
+                                access(new Runnable() {
+                                    @Override
+                                    public void run() {
+                                        log("Access has current response? "
+                                                + (VaadinService
+                                                        .getCurrentResponse() != null));
+                                        log("Thread is still alive? "
+                                                + thisThread.isAlive());
+                                    }
+                                });
+                            }
+                        }.start();
+
+                        // Wait for thread to do initialize before continuing
+                        try {
+                            latch.await();
+                        } catch (InterruptedException e) {
+                            throw new RuntimeException(e);
+                        }
+
+                        setPollInterval(3000);
+                    }
+                }));
+        addComponent(new Button("Access throwing exception",
+                new Button.ClickListener() {
+                    @Override
+                    public void buttonClick(ClickEvent event) {
+                        log.clear();
+                        final Future<Void> firstFuture = access(new Runnable() {
+                            @Override
+                            public void run() {
+                                log("Throwing exception in access");
+                                throw new RuntimeException(
+                                        "Catch me if you can");
+                            }
+                        });
+                        access(new Runnable() {
+                            @Override
+                            public void run() {
+                                log("firstFuture is done? "
+                                        + firstFuture.isDone());
+                                try {
+                                    firstFuture.get();
+                                    log("Should not get here");
+                                } catch (InterruptedException e) {
+                                    throw new RuntimeException(e);
+                                } catch (ExecutionException e) {
+                                    log("Got exception from firstFuture: "
+                                            + e.getMessage());
+                                }
+                            }
+                        });
+                    }
+                }));
+        addComponent(new Button("Cancel future before started",
+                new Button.ClickListener() {
+                    @Override
+                    public void buttonClick(ClickEvent event) {
+                        log.clear();
+                        Future<Void> future = access(new Runnable() {
+                            @Override
+                            public void run() {
+                                log("Should not get here");
+                            }
+                        });
+                        future.cancel(false);
+                        log("future was cancled, should not start");
+                    }
+                }));
+        addComponent(new Button("Cancel running future",
+                new Button.ClickListener() {
+                    @Override
+                    public void buttonClick(ClickEvent event) {
+                        log.clear();
+                        final ReentrantLock interruptLock = new ReentrantLock();
+
+                        final Future<Void> future = access(new Runnable() {
+                            @Override
+                            public void run() {
+                                log("Waiting for thread to start");
+                                while (!interruptLock.isLocked()) {
+                                    try {
+                                        Thread.sleep(100);
+                                    } catch (InterruptedException e) {
+                                        log("Premature interruption");
+                                        throw new RuntimeException(e);
+                                    }
+                                }
+
+                                log("Thread started, waiting for interruption");
+                                try {
+                                    interruptLock.lockInterruptibly();
+                                } catch (InterruptedException e) {
+                                    log("I was interrupted");
+                                }
+                            }
+                        });
+
+                        new Thread() {
+                            @Override
+                            public void run() {
+                                interruptLock.lock();
+                                // Wait until UI thread has started waiting for
+                                // the lock
+                                while (!interruptLock.hasQueuedThreads()) {
+                                    try {
+                                        Thread.sleep(100);
+                                    } catch (InterruptedException e) {
+                                        throw new RuntimeException(e);
+                                    }
+                                }
+
+                                future.cancel(true);
+                            }
+                        }.start();
+                    }
+                }));
+    }
+
+    @Override
+    public void beforeClientResponse(boolean initial) {
+        if (checkFromBeforeClientResponse != null) {
+            log("beforeClientResponse future is done? "
+                    + checkFromBeforeClientResponse.isDone());
+            checkFromBeforeClientResponse = null;
+        }
+    }
+
+    @Override
+    protected String getTestDescription() {
+        return "Test for various ways of using UI.access";
+    }
+
+    @Override
+    protected Integer getTicketNumber() {
+        return Integer.valueOf(11897);
+    }
+
+}