]> source.dussan.org Git - vaadin-framework.git/commitdiff
Workaround for deadlock issue (#18436)
authorArtur Signell <artur@vaadin.com>
Thu, 6 Oct 2016 20:07:20 +0000 (23:07 +0300)
committerArtur Signell <artur@vaadin.com>
Tue, 11 Oct 2016 08:30:21 +0000 (11:30 +0300)
Change-Id: I4e32550e3d3095c2c914bb93d260819414d2e6e6

server/src/main/java/com/vaadin/ui/UI.java
server/src/test/java/com/vaadin/ui/UITest.java [new file with mode: 0644]
uitest/src/main/java/com/vaadin/tests/push/PushWebsocketDeadlockUI.java [new file with mode: 0644]

index fcae3c7b2e489ff0d87569ba0f39920405e08362..24a53dfd9f775672f79e99ec5c4bfc5383834556 100644 (file)
@@ -470,9 +470,23 @@ public abstract class UI extends AbstractSingleComponentContainer
                             "Error while detaching UI from session", e);
                 }
                 // Disable push when the UI is detached. Otherwise the
-                // push connection and possibly VaadinSession will live on.
+                // push connection and possibly VaadinSession will live
+                // on.
                 getPushConfiguration().setPushMode(PushMode.DISABLED);
-                setPushConnection(null);
+
+                new Thread(new Runnable() {
+                    @Override
+                    public void run() {
+                        // This intentionally does disconnect without locking
+                        // the VaadinSession to avoid deadlocks where the server
+                        // uses a lock for the websocket connection
+
+                        // See https://dev.vaadin.com/ticket/18436
+                        // The underlying problem is
+                        // https://dev.vaadin.com/ticket/16919
+                        setPushConnection(null);
+                    }
+                }).start();
             }
             this.session = session;
         }
diff --git a/server/src/test/java/com/vaadin/ui/UITest.java b/server/src/test/java/com/vaadin/ui/UITest.java
new file mode 100644 (file)
index 0000000..322bc81
--- /dev/null
@@ -0,0 +1,155 @@
+package com.vaadin.ui;
+
+import java.util.Properties;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.servlet.ServletConfig;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.server.DefaultDeploymentConfiguration;
+import com.vaadin.server.MockServletConfig;
+import com.vaadin.server.MockVaadinSession;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.server.VaadinServlet;
+import com.vaadin.server.VaadinServletService;
+import com.vaadin.server.VaadinSession;
+import com.vaadin.server.communication.PushConnection;
+import com.vaadin.shared.communication.PushMode;
+
+public class UITest {
+
+    @Test
+    public void removeFromSessionWithExternalLock() throws Exception {
+        // See https://dev.vaadin.com/ticket/18436
+        final UI ui = new UI() {
+
+            @Override
+            protected void init(VaadinRequest request) {
+            }
+
+        };
+        final Lock externalLock = new ReentrantLock();
+
+        ServletConfig servletConfig = new MockServletConfig();
+        VaadinServlet servlet = new VaadinServlet();
+        servlet.init(servletConfig);
+
+        DefaultDeploymentConfiguration deploymentConfiguration = new DefaultDeploymentConfiguration(
+                UI.class, new Properties());
+
+        MockVaadinSession session = new MockVaadinSession(
+                new VaadinServletService(servlet, deploymentConfiguration));
+        session.lock();
+        ui.setSession(session);
+        ui.getPushConfiguration().setPushMode(PushMode.MANUAL);
+        ui.setPushConnection(new PushConnection() {
+
+            private boolean connected = true;
+
+            @Override
+            public void push() {
+            }
+
+            @Override
+            public boolean isConnected() {
+                return connected;
+            }
+
+            @Override
+            public void disconnect() {
+                externalLock.lock();
+                try {
+                    connected = false;
+                } finally {
+                    externalLock.unlock();
+                }
+
+            }
+        });
+        session.unlock();
+
+        final CountDownLatch websocketReachedCheckpoint = new CountDownLatch(1);
+        final CountDownLatch uiDisconnectReachedCheckpoint = new CountDownLatch(
+                1);
+
+        final VaadinSession uiSession = ui.getSession();
+        final ConcurrentLinkedQueue<Exception> exceptions = new ConcurrentLinkedQueue<Exception>();
+
+        // Simulates the websocket close thread
+        Runnable websocketClose = new Runnable() {
+            @Override
+            public void run() {
+                externalLock.lock();
+                // Wait for disconnect thread to lock VaadinSession
+                websocketReachedCheckpoint.countDown();
+                try {
+                    uiDisconnectReachedCheckpoint.await();
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                    exceptions.add(e);
+                    return;
+                }
+                uiSession.lock();
+                externalLock.unlock();
+            }
+        };
+
+        Runnable disconnectPushFromUI = new Runnable() {
+            @Override
+            public void run() {
+                uiSession.lock();
+                // Wait for websocket thread to lock external lock
+                uiDisconnectReachedCheckpoint.countDown();
+                try {
+                    websocketReachedCheckpoint.await();
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                    exceptions.add(e);
+                    return;
+                }
+
+                ui.setSession(null);
+                uiSession.unlock();
+            }
+        };
+
+        Thread websocketThread = new Thread(websocketClose);
+        websocketThread.start();
+        Thread uiDisconnectThread = new Thread(disconnectPushFromUI);
+        uiDisconnectThread.start();
+
+        websocketThread.join(5000);
+        uiDisconnectThread.join(5000);
+
+        if (websocketThread.isAlive() || uiDisconnectThread.isAlive()) {
+            websocketThread.interrupt();
+            uiDisconnectThread.interrupt();
+            Assert.fail("Threads are still running");
+        }
+        if (!exceptions.isEmpty()) {
+            for (Exception e : exceptions) {
+                e.printStackTrace();
+            }
+            Assert.fail("There were exceptions in the threads");
+        }
+
+        Assert.assertNull(ui.getSession());
+
+        // PushConnection is set to null in another thread. We need to wait for
+        // that to happen
+        for (int i = 0; i < 10; i++) {
+            if (ui.getPushConnection() == null) {
+                break;
+            }
+
+            Thread.sleep(500);
+        }
+        Assert.assertNull(ui.getPushConnection());
+
+    }
+}
diff --git a/uitest/src/main/java/com/vaadin/tests/push/PushWebsocketDeadlockUI.java b/uitest/src/main/java/com/vaadin/tests/push/PushWebsocketDeadlockUI.java
new file mode 100644 (file)
index 0000000..4d6e9c4
--- /dev/null
@@ -0,0 +1,120 @@
+package com.vaadin.tests.push;
+
+import com.vaadin.annotations.Push;
+import com.vaadin.server.SessionDestroyEvent;
+import com.vaadin.server.SessionDestroyListener;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.server.WrappedSession;
+import com.vaadin.shared.ui.ui.Transport;
+import com.vaadin.tests.components.AbstractTestUIWithLog;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Button.ClickEvent;
+import com.vaadin.ui.Button.ClickListener;
+import com.vaadin.ui.Label;
+
+@Push(transport = Transport.WEBSOCKET)
+public class PushWebsocketDeadlockUI extends AbstractTestUIWithLog {
+
+    // Test for https://dev.vaadin.com/ticket/18436
+    // Needs breakpoints to test, see ticket for more information
+    // Can reproduce on Tomcat 8, can't seem to reproduce using
+    // DevelopmentServerLauncher
+
+    // Rough steps to reproduce
+    // 1. Open test in a new Chrome window
+    // 2. Set breakpoint in PushHandler.connectionLost
+    // 3. Set breakpoint in UI.close
+    // 4. Set breakpoint in PushRequestHandler.handleRequest
+    // 5. Click the "schedule UI close" button
+    // 6. Close the Chrome window before the 5s timeout expires and ensure it
+    // really closes
+    // 7. Wait for three threads to hit their breakpoints
+    // 8. Continue/step forward in proper order (see ticket)
+
+    @Override
+    protected void setup(VaadinRequest request) {
+        WrappedSession wrappedSession = getSession().getSession();
+        request.getService()
+                .addSessionDestroyListener(new SessionDestroyListener() {
+                    @Override
+                    public void sessionDestroy(SessionDestroyEvent e) {
+                        System.out.println(
+                                "Session " + e.getSession() + " destroyed");
+                    }
+                });
+        final Label l = new Label("Session timeout is "
+                + wrappedSession.getMaxInactiveInterval() + "s");
+        addComponents(l);
+
+        Button button = new Button("Invalidate session");
+        button.addClickListener(new ClickListener() {
+            @Override
+            public void buttonClick(ClickEvent e) {
+                System.out.println(
+                        "invalidating " + getSession() + " for http session "
+                                + getSession().getSession().getId());
+                getSession().getSession().invalidate();
+                System.out.println("invalidated " + getSession());
+            }
+        });
+        addComponents(button);
+        button = new Button("Close UI");
+        button.addClickListener(new ClickListener() {
+            @Override
+            public void buttonClick(ClickEvent e) {
+                System.out.println("closing UI " + getUIId() + " in session "
+                        + getSession() + " for http session "
+                        + getSession().getSession().getId());
+                close();
+            }
+        });
+        addComponents(button);
+        button = new Button("Schedule Close UI (5s delay)");
+        button.addClickListener(new ClickListener() {
+            @Override
+            public void buttonClick(ClickEvent e) {
+                new Thread(new Runnable() {
+                    @Override
+                    public void run() {
+                        try {
+                            Thread.sleep(5000);
+                        } catch (InterruptedException e1) {
+                            // TODO Auto-generated catch block
+                            e1.printStackTrace();
+                        }
+                        // Breakpoint here
+                        access(new Runnable() {
+                            @Override
+                            public void run() {
+                                close();
+                                System.out.println("closing UI " + getUIId()
+                                        + " in session " + getSession()
+                                        + " for http session "
+                                        + getSession().getSession().getId());
+
+                            }
+                        });
+
+                    }
+                }).start();
+            }
+        });
+        addComponents(button);
+        button = new Button("Slow (5s) operation");
+        button.addClickListener(new ClickListener() {
+            @Override
+            public void buttonClick(ClickEvent e) {
+                try {
+                    Thread.sleep(5000);
+                } catch (InterruptedException e1) {
+                    e1.printStackTrace();
+                }
+                addComponent(new Label("Slow operation done"));
+            }
+        });
+
+        addComponents(button);
+
+    }
+
+}