diff options
author | Leif Åstrand <leif@vaadin.com> | 2015-06-20 13:59:04 +0300 |
---|---|---|
committer | Teemu Suo-Anttila <teemusa@vaadin.com> | 2015-07-21 14:40:17 +0300 |
commit | 80d4968feff9a36b7734344182f8bd82cf363053 (patch) | |
tree | e1621f172a26e59c599f2f589d7c2fdab6b769de | |
parent | 517485502af1f86358a44bd7067389d4b55aeba8 (diff) | |
download | vaadin-framework-80d4968feff9a36b7734344182f8bd82cf363053.tar.gz vaadin-framework-80d4968feff9a36b7734344182f8bd82cf363053.zip |
Make async remove check work without push (#12909)
Change-Id: I8d641d55de873283de5ba32f12c304622ce24152
4 files changed, 166 insertions, 29 deletions
diff --git a/server/src/com/vaadin/ui/ConnectorTracker.java b/server/src/com/vaadin/ui/ConnectorTracker.java index 95a80b7be0..eba248fb00 100644 --- a/server/src/com/vaadin/ui/ConnectorTracker.java +++ b/server/src/com/vaadin/ui/ConnectorTracker.java @@ -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 index 0000000000..d8f7fface3 --- /dev/null +++ b/uitest/src/com/vaadin/tests/application/ResynchronizeAfterAsyncRemoval.java @@ -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 index 0000000000..7f2dabe9f1 --- /dev/null +++ b/uitest/src/com/vaadin/tests/application/ResynchronizeAfterAsyncRemovalTest.java @@ -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)); + } +} diff --git a/uitest/src/com/vaadin/tests/components/AbstractTestUI.java b/uitest/src/com/vaadin/tests/components/AbstractTestUI.java index dba055a65a..98b0f63ce1 100644 --- a/uitest/src/com/vaadin/tests/components/AbstractTestUI.java +++ b/uitest/src/com/vaadin/tests/components/AbstractTestUI.java @@ -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(); + } } |