]> source.dussan.org Git - vaadin-framework.git/commitdiff
Make async remove check work without push (#12909)
authorLeif Åstrand <leif@vaadin.com>
Sat, 20 Jun 2015 10:59:04 +0000 (13:59 +0300)
committerVaadin Code Review <review@vaadin.com>
Sat, 11 Jul 2015 13:16:02 +0000 (13:16 +0000)
Change-Id: Ie5843c7fb5bb6365ceef998206df69302046e686

server/src/com/vaadin/ui/ConnectorTracker.java
uitest/src/com/vaadin/tests/application/ResynchronizeAfterAsyncRemoval.java [new file with mode: 0644]
uitest/src/com/vaadin/tests/application/ResynchronizeAfterAsyncRemovalTest.java [new file with mode: 0644]
uitest/src/com/vaadin/tests/components/AbstractTestUI.java

index 95a80b7be0a9f2c988317537f4ac10fef237dc7c..eba248fb0073f72700798e735f25dd1ece29aba4 100644 (file)
@@ -24,6 +24,7 @@ import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.Map;
+import java.util.NavigableMap;
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.UUID;
@@ -591,8 +592,8 @@ public class ConnectorTracker implements Serializable {
      * <p>
      * This method has a side-effect of incrementing the sync id by one (see
      * {@link #getCurrentSyncId()}), if {@link #isWritingResponse()} returns
-     * <code>false</code> and <code>writingResponse</code> is set to
-     * <code>true</code>.
+     * <code>true</code> and <code>writingResponse</code> is set to
+     * <code>false</code>.
      *
      * @param writingResponse
      *            the new response status.
@@ -616,7 +617,9 @@ public class ConnectorTracker implements Serializable {
          * the right hand side of the && is unnecessary here because of the
          * if-clause above, but rigorous coding is always rigorous coding.
          */
-        if (writingResponse && !this.writingResponse) {
+        if (!writingResponse && this.writingResponse) {
+            // Bump sync id when done writing - the client is not expected to
+            // know about anything happening after this moment.
             currentSyncId++;
         }
         this.writingResponse = writingResponse;
@@ -784,34 +787,25 @@ public class ConnectorTracker implements Serializable {
      */
     public boolean connectorWasPresentAsRequestWasSent(String connectorId,
             long lastSyncIdSeenByClient) {
-
         assert getConnector(connectorId) == null : "Connector " + connectorId
                 + " is still attached";
 
-        boolean clientRequestIsTooOld = lastSyncIdSeenByClient < currentSyncId
-                && lastSyncIdSeenByClient != -1;
-        if (clientRequestIsTooOld) {
-            /*
-             * The headMap call is present here because we're only interested in
-             * connectors removed "in the past" (i.e. the server has removed
-             * them before the client ever knew about that), since those are the
-             * ones that we choose to handle as a special case.
-             */
-            /*-
-             *   Server                          Client
-             * [#1 add table] ---------.
-             *                          \
-             * [push: #2 remove table]-. `--> [adding table, storing #1]
-             *                          \  .- [table from request #1 needs more data]
-             *                           \/
-             *                           /`-> [removing table, storing #2]
-             * [#1 < #2 - ignoring] <---´
-             */
-            for (Set<String> unregisteredConnectors : syncIdToUnregisteredConnectorIds
-                    .headMap(currentSyncId).values()) {
-                if (unregisteredConnectors.contains(connectorId)) {
-                    return true;
-                }
+        if (lastSyncIdSeenByClient == -1) {
+            // Ignore potential problems
+            return true;
+        }
+
+        /*
+         * Use non-inclusive tail map to find all connectors that were removed
+         * after the reported sync id was sent to the client.
+         */
+        NavigableMap<Integer, Set<String>> unregisteredAfter = syncIdToUnregisteredConnectorIds
+                .tailMap(Integer.valueOf((int) lastSyncIdSeenByClient), false);
+        for (Set<String> unregisteredIds : unregisteredAfter.values()) {
+            if (unregisteredIds.contains(connectorId)) {
+                // Removed with a higher sync id, so it was most likely present
+                // when this sync id was sent.
+                return true;
             }
         }
 
@@ -877,7 +871,7 @@ public class ConnectorTracker implements Serializable {
          * conflicts. In any case, it's better to clean up too little than too
          * much, especially as the data will hardly grow into the kilobytes.
          */
-        syncIdToUnregisteredConnectorIds.headMap(lastSyncIdSeenByClient)
+        syncIdToUnregisteredConnectorIds.headMap(lastSyncIdSeenByClient, true)
                 .clear();
     }
 }
diff --git a/uitest/src/com/vaadin/tests/application/ResynchronizeAfterAsyncRemoval.java b/uitest/src/com/vaadin/tests/application/ResynchronizeAfterAsyncRemoval.java
new file mode 100644 (file)
index 0000000..d8f7ffa
--- /dev/null
@@ -0,0 +1,77 @@
+package com.vaadin.tests.application;
+
+import java.lang.reflect.Field;
+import java.util.Map;
+import java.util.Set;
+
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUIWithLog;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Button.ClickEvent;
+import com.vaadin.ui.ConnectorTracker;
+import com.vaadin.ui.Window;
+
+public class ResynchronizeAfterAsyncRemoval extends AbstractTestUIWithLog {
+
+    @Override
+    public void setup(VaadinRequest vaadinRequest) {
+        final Window window = new Window("Asynchronously removed window");
+        window.center();
+
+        // The window will enqueue a non-immediate message reporting its current
+        // position.
+        addWindow(window);
+
+        // Remove window immediately when the current response is sent
+        runAfterResponse(new Runnable() {
+            @Override
+            public void run() {
+                removeWindow(window);
+            }
+        });
+
+        // Clicking the button will trigger sending the window coordinates, but
+        // the window is already removed at that point.
+        addComponent(new Button("Am I dirty?", new Button.ClickListener() {
+            @Override
+            public void buttonClick(ClickEvent event) {
+                log("Window removed: " + (window.getParent() == null));
+
+                boolean dirty = getUI().getConnectorTracker().isDirty(
+                        event.getButton());
+                log("Dirty: " + dirty);
+            }
+        }));
+        addComponent(new Button("Log unregistered connector count",
+                new Button.ClickListener() {
+                    @Override
+                    public void buttonClick(ClickEvent event) {
+                        logUnregisteredConnectorCount();
+                    }
+                }));
+    }
+
+    private void logUnregisteredConnectorCount() {
+        int count = 0;
+
+        Map<Integer, Set<String>> unregisterIdMap = getUnregisterIdMap();
+        for (Set<String> set : unregisterIdMap.values()) {
+            count += set.size();
+        }
+        log("syncId: " + getConnectorTracker().getCurrentSyncId());
+        log("Unregistered connector count: " + count);
+    }
+
+    @SuppressWarnings("unchecked")
+    private Map<Integer, Set<String>> getUnregisterIdMap() {
+        try {
+            ConnectorTracker tracker = getConnectorTracker();
+            Field field = tracker.getClass().getDeclaredField(
+                    "syncIdToUnregisteredConnectorIds");
+            field.setAccessible(true);
+            return (Map<Integer, Set<String>>) field.get(tracker);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
\ No newline at end of file
diff --git a/uitest/src/com/vaadin/tests/application/ResynchronizeAfterAsyncRemovalTest.java b/uitest/src/com/vaadin/tests/application/ResynchronizeAfterAsyncRemovalTest.java
new file mode 100644 (file)
index 0000000..7f2dabe
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2000-2014 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.application;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.testbench.elements.ButtonElement;
+import com.vaadin.tests.tb3.SingleBrowserTest;
+
+public class ResynchronizeAfterAsyncRemovalTest extends SingleBrowserTest {
+    @Test
+    public void noResyncAfterAsyncRemoval() {
+        openTestURL();
+
+        $(ButtonElement.class).first().click();
+
+        Assert.assertEquals("Timing issue in the test?",
+                "1. Window removed: true", getLogRow(1));
+
+        Assert.assertEquals(
+                "Removing window should not cause button to be marked as dirty",
+                "2. Dirty: false", getLogRow(0));
+
+        ButtonElement logCountButton = $(ButtonElement.class).all().get(1);
+        logCountButton.click();
+
+        Assert.assertEquals("Sanity check", "3. syncId: 2", getLogRow(1));
+        Assert.assertEquals("Sanity check",
+                "4. Unregistered connector count: 1", getLogRow(0));
+
+        logCountButton.click();
+
+        Assert.assertEquals("Sanity check", "5. syncId: 3", getLogRow(1));
+        Assert.assertEquals(
+                "Unregistered connector map should have been cleared",
+                "6. Unregistered connector count: 0", getLogRow(0));
+    }
+}
index dba055a65a06e4a543b5f06efafbed16e4c2015b..98b0f63ce1da21f10b0f45d25ffea07af5396a37 100644 (file)
@@ -205,4 +205,18 @@ public abstract class AbstractTestUI extends UI {
         return getSession().getBrowser();
     }
 
+    /**
+     * Execute the provided runnable on the UI thread as soon as the current
+     * request has been sent.
+     */
+    protected void runAfterResponse(final Runnable runnable) {
+        // Immediately start a thread that will start waiting for the session to
+        // get unlocked.
+        new Thread() {
+            @Override
+            public void run() {
+                accessSynchronously(runnable);
+            }
+        }.start();
+    }
 }