.handleServerResponse(json.getValueMap("dd"));
}
- int removed = unregisterRemovedConnectors(
+ unregisterRemovedConnectors(
connectorHierarchyUpdateResult.detachedConnectorIds);
- if (removed > 0 && !isResponse(json)) {
- // Must acknowledge the removal using an XHR or server
- // memory usage will keep growing
- getUIConnector().sendAck();
- }
+
getLogger().info("handleUIDLMessage: "
+ (Duration.currentTimeMillis() - processUidlStart)
+ " ms");
"verifyConnectorHierarchy - this is only performed in debug mode");
}
- private int unregisterRemovedConnectors(
+ private void unregisterRemovedConnectors(
FastStringSet detachedConnectors) {
Profiler.enter("unregisterRemovedConnectors");
JsArrayString detachedArray = detachedConnectors.dump();
- int nrDetached = detachedArray.length();
- for (int i = 0; i < nrDetached; i++) {
- ServerConnector connector = getConnectorMap()
- .getConnector(detachedArray.get(i));
+ for (int i = 0; i < detachedArray.length(); i++) {
+ ServerConnector connector = getConnectorMap().getConnector(
+ detachedArray.get(i));
Profiler.enter(
"unregisterRemovedConnectors unregisterConnector");
verifyConnectorHierarchy();
}
- getLogger()
- .info("* Unregistered " + nrDetached + " connectors");
+ getLogger().info("* Unregistered " + detachedArray.length()
+ + " connectors");
Profiler.leave("unregisterRemovedConnectors");
- return nrDetached;
}
private JsArrayString createConnectorsIfNeeded(ValueMap json) {
private static Logger getLogger() {
return Logger.getLogger(UIConnector.class.getName());
}
-
- /**
- * Send an acknowledgement RPC to the server. This allows the server to know
- * which messages the client has received, even when the client is not
- * sending any other traffic.
- */
- public void sendAck() {
- getRpcProxy(UIServerRpc.class).acknowledge();
-
- }
}
* The parent to set
*/
private void internalSetParent(ClientConnector parent) {
+ ClientConnector oldParent = getParent();
// Send a detach event if the component is currently attached
if (isAttached()) {
attach();
}
+ if (oldParent != null) {
+ oldParent.markAsDirty();
+ }
}
@Override
rpcRequest.getRpcInvocationsData());
}
- ui.getConnectorTracker()
- .cleanConcurrentlyRemovedConnectorIds(rpcRequest.getSyncId());
-
if (rpcRequest.isResynchronize()) {
ui.getSession().getCommunicationManager().repaintAll(ui);
}
String interfaceName = invocationJson.getString(1);
String methodName = invocationJson.getString(2);
- if (connectorTracker.getConnector(connectorId) == null && !connectorId
- .equals(ApplicationConstants.DRAG_AND_DROP_CONNECTOR_ID)) {
-
- if (!connectorTracker.connectorWasPresentAsRequestWasSent(
- connectorId, lastSyncIdSeenByClient)) {
- getLogger().log(Level.WARNING, "RPC call to " + interfaceName
- + "." + methodName + " received for connector "
- + connectorId
- + " but no such connector could be found. Resynchronizing client.");
- // This is likely an out of sync issue (client tries to update a
- // connector which is not present). Force resync.
- connectorTracker.markAllConnectorsDirty();
- }
- return null;
- }
-
JsonArray parametersJson = invocationJson.getArray(3);
if (LegacyChangeVariablesInvocation
import com.vaadin.event.ShortcutListener;
import com.vaadin.server.AbstractClientConnector;
import com.vaadin.server.AbstractErrorMessage.ContentMode;
+import com.vaadin.server.ClientConnector;
import com.vaadin.server.ComponentSizeValidator;
import com.vaadin.server.ErrorMessage;
import com.vaadin.server.ErrorMessage.ErrorLevel;
getClass().getName() + " already has a parent.");
}
+ ClientConnector oldParent = getParent();
+
// Send a detach event if the component is currently attached
if (isAttached()) {
detach();
if (isAttached()) {
attach();
}
+
+ if (oldParent != null) {
+ oldParent.markAsDirty();
+ }
}
/**
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;
import java.util.logging.Level;
import java.util.logging.Logger;
private int currentSyncId = 0;
- /**
- * Map to track on which syncId each connector was removed.
- *
- * @see #getCurrentSyncId()
- * @see #cleanConcurrentlyRemovedConnectorIds(long)
- */
- private TreeMap<Integer, Set<String>> syncIdToUnregisteredConnectorIds = new TreeMap<Integer, Set<String>>();
-
/**
* Gets a logger for this class
*
+ " is not the one that was registered for that id");
}
- Set<String> unregisteredConnectorIds = syncIdToUnregisteredConnectorIds
- .get(currentSyncId);
- if (unregisteredConnectorIds == null) {
- unregisteredConnectorIds = new HashSet<String>();
- syncIdToUnregisteredConnectorIds.put(currentSyncId,
- unregisteredConnectorIds);
- }
- unregisteredConnectorIds.add(connectorId);
-
dirtyConnectors.remove(connector);
if (unregisteredConnectors.add(connector)) {
if (getLogger().isLoggable(Level.FINE)) {
return streamVariableToSeckey.get(variable);
}
- /**
- * Check whether a connector was present on the client when the it was
- * creating this request, but was removed server-side before the request
- * arrived.
- *
- * @since 7.2
- * @param connectorId
- * The connector id to check for whether it was removed
- * concurrently or not.
- * @param lastSyncIdSeenByClient
- * the most recent sync id the client has seen at the time the
- * request was sent, or -1 to ignore potential problems
- * @return <code>true</code> if the connector was removed before the client
- * had a chance to react to it.
- */
- public boolean connectorWasPresentAsRequestWasSent(String connectorId,
- long lastSyncIdSeenByClient) {
- assert getConnector(connectorId) == null : "Connector " + connectorId
- + " is still attached";
-
- 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;
- }
- }
-
- return false;
- }
-
/**
* Gets the most recently generated server sync id.
* <p>
public int getCurrentSyncId() {
return currentSyncId;
}
-
- /**
- * Maintains the bookkeeping connector removal and concurrency by removing
- * entries that have become too old.
- * <p>
- * <em>It is important to run this call for each transmission from the
- * client</em> , otherwise the bookkeeping gets out of date and the results
- * form {@link #connectorWasPresentAsRequestWasSent(String, long)} will
- * become invalid (that is, even though the client knew the component was
- * removed, the aforementioned method would start claiming otherwise).
- * <p>
- * Entries that both client and server agree upon are removed. Since
- * argument is the last sync id that the client has seen from the server, we
- * know that entries earlier than that cannot cause any problems anymore.
- * <p>
- * The sync id value <code>-1</code> is ignored to facilitate testing with
- * pre-recorded requests.
- *
- * @see #connectorWasPresentAsRequestWasSent(String, long)
- * @since 7.2
- * @param lastSyncIdSeenByClient
- * the sync id the client has most recently received from the
- * server.
- */
- public void cleanConcurrentlyRemovedConnectorIds(
- int lastSyncIdSeenByClient) {
- if (lastSyncIdSeenByClient == -1) {
- // Sync id checking is not in use, so we should just clear the
- // entire map to avoid leaking memory
- syncIdToUnregisteredConnectorIds.clear();
- return;
- }
- /*
- * We remove all entries _older_ than the one reported right now,
- * because the remaining still contain components that might cause
- * 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, true)
- .clear();
- }
}
public void poll() {
fireEvent(new PollEvent(UI.this));
}
-
- @Override
- public void acknowledge() {
- // Nothing to do, just need the message to be sent and processed
- }
};
private DebugWindowServerRpc debugRpc = new DebugWindowServerRpc() {
@Override
--- /dev/null
+package com.vaadin.tests.server.abstractextension;
+
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import com.vaadin.server.AbstractExtension;
+import com.vaadin.server.ClientConnector;
+
+public class AbstractExtensionSetParentTest {
+
+ private static class TestExtension extends AbstractExtension {
+
+ }
+
+ @Test
+ public void setParent_marks_old_parent_as_dirty() {
+ ClientConnector connector = Mockito.mock(ClientConnector.class);
+ TestExtension extension = new TestExtension();
+ extension.setParent(connector);
+ extension.setParent(null);
+ Mockito.verify(connector, Mockito.times(1)).markAsDirty();
+ }
+}
--- /dev/null
+package com.vaadin.tests.server.component.abstractcomponent;
+
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import com.vaadin.ui.AbstractComponent;
+import com.vaadin.ui.HasComponents;
+
+public class AbstractComponentSetParentTest {
+
+ private static class TestComponent extends AbstractComponent {
+ }
+
+ @Test
+ public void setParent_marks_old_parent_as_dirty() {
+ HasComponents hasComponents = Mockito.mock(HasComponents.class);
+ TestComponent testComponent = new TestComponent();
+ testComponent.setParent(hasComponents);
+ testComponent.setParent(null);
+ Mockito.verify(hasComponents, Mockito.times(1)).markAsDirty();
+ }
+}
* should always be called to ensure the message is flushed right away.
*/
public void poll();
-
- @NoLoadingIndicator
- public void acknowledge();
}
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 {
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
+++ /dev/null
-package com.vaadin.tests.components;
-
-import com.vaadin.server.VaadinRequest;
-import com.vaadin.ui.Button;
-import com.vaadin.ui.Button.ClickEvent;
-import com.vaadin.ui.Button.ClickListener;
-import com.vaadin.ui.Notification;
-
-public class OutOfSync extends AbstractTestUI {
-
- @Override
- protected void setup(VaadinRequest request) {
- Button b = new Button("Click me after 1s to be out of sync");
- b.addClickListener(new ClickListener() {
-
- @Override
- public void buttonClick(ClickEvent event) {
- Notification.show("This code will never be reached");
- }
- });
- setContent(b);
- Thread t = new Thread(new Runnable() {
-
- @Override
- public void run() {
- try {
- Thread.sleep(500);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- // Remove button but prevent repaint -> causes out of sync
- // issues
- getSession().lock();
- try {
- setContent(null);
- getConnectorTracker().markClean(OutOfSync.this);
- } finally {
- getSession().unlock();
- }
- }
- });
- t.start();
- }
-
- @Override
- protected String getTestDescription() {
- return "Click the button after 1s when it has been removed server side (causing synchronization problems)";
- }
-
- @Override
- protected Integer getTicketNumber() {
- return 10780;
- }
-
-}
import com.vaadin.ui.Button.ClickEvent;
import com.vaadin.ui.ConnectorTracker;
import com.vaadin.ui.Table;
+
import elemental.json.JsonObject;
@Push
return tracker.getSeckey(variable);
}
- @Override
- public boolean connectorWasPresentAsRequestWasSent(String connectorId,
- long lastSyncIdSeenByClient) {
- return tracker.connectorWasPresentAsRequestWasSent(connectorId,
- lastSyncIdSeenByClient);
- }
-
@Override
public int getCurrentSyncId() {
return tracker.getCurrentSyncId();
}
- @Override
- public void cleanConcurrentlyRemovedConnectorIds(
- int lastSyncIdSeenByClient) {
- tracker.cleanConcurrentlyRemovedConnectorIds(
- lastSyncIdSeenByClient);
- }
-
@Override
public boolean equals(Object obj) {
return tracker.equals(obj);
+++ /dev/null
-package com.vaadin.tests.push;
-
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.commons.lang.SerializationUtils;
-
-import com.vaadin.annotations.Push;
-import com.vaadin.data.Property.ValueChangeEvent;
-import com.vaadin.data.Property.ValueChangeListener;
-import com.vaadin.server.VaadinRequest;
-import com.vaadin.tests.components.AbstractTestUIWithLog;
-import com.vaadin.ui.AbstractOrderedLayout;
-import com.vaadin.ui.Button;
-import com.vaadin.ui.Button.ClickEvent;
-import com.vaadin.ui.Button.ClickListener;
-import com.vaadin.ui.CheckBox;
-import com.vaadin.ui.HorizontalLayout;
-import com.vaadin.ui.Label;
-
-@Push
-public class PushRemoveConnectors extends AbstractTestUIWithLog {
-
- private transient final ScheduledExecutorService threadPool = Executors
- .newScheduledThreadPool(5);
- static final String START = "start";
- static final String STOP = "stop";
- private AbstractOrderedLayout verticalLayout;
- private transient ScheduledFuture<?> task = null;
-
- @Override
- protected void setup(VaadinRequest request) {
- final CheckBox pollingEnabled = new CheckBox("Polling enabled");
- pollingEnabled.addValueChangeListener(new ValueChangeListener() {
- @Override
- public void valueChange(ValueChangeEvent event) {
- setPollInterval(pollingEnabled.getValue() ? 1000 : -1);
- }
- });
-
- Button start = new Button("start");
- start.setId(START);
- start.addClickListener(new ClickListener() {
-
- @Override
- public void buttonClick(ClickEvent event) {
- task = threadPool.scheduleAtFixedRate(new Runnable() {
- @Override
- public void run() {
- access(new Runnable() {
- public void run() {
- populate();
- log("Serialized session size: "
- + getSessionSize());
- }
- });
- }
- }, 1, 1, TimeUnit.SECONDS);
- }
- });
- Button stop = new Button("stop");
- stop.setId(STOP);
- stop.addClickListener(new ClickListener() {
- @Override
- public void buttonClick(ClickEvent event) {
- if (task != null) {
- task.cancel(true);
- task = null;
- }
-
- }
- });
- verticalLayout = new HorizontalLayout();
- populate();
- addComponents(pollingEnabled, start, stop, verticalLayout);
- }
-
- private void populate() {
- verticalLayout.removeAllComponents();
- for (int i = 0; i < 500; i++) {
- Label l = new Label(".");
- l.setSizeUndefined();
- verticalLayout.addComponent(l);
- }
- }
-
- private int getSessionSize() {
- return SerializationUtils.serialize(getSession()).length;
- }
-}
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));
}
}
+++ /dev/null
-/*
- * 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.components;
-
-import org.junit.Assert;
-import org.junit.Test;
-
-import com.vaadin.testbench.elements.ButtonElement;
-import com.vaadin.tests.tb3.MultiBrowserTest;
-
-public class OutOfSyncTest extends MultiBrowserTest {
-
- @Test
- public void testClientResync() throws InterruptedException {
- openTestURL();
-
- // Wait for server to get rid of the Button
- sleep(1000);
-
- // On the first round-trip after the component has been removed, the
- // server assumes the client will remove the button. How ever (to force
- // it to be out of sync) the test UI calls markClean() on the Button to
- // make it not update with the response.
- $(ButtonElement.class).first().click();
- Assert.assertTrue(
- "Button should not have disappeared on the first click.",
- $(ButtonElement.class).exists());
-
- // Truly out of sync, full resync is forced.
- $(ButtonElement.class).first().click();
- Assert.assertFalse("Button should disappear with the second click.",
- $(ButtonElement.class).exists());
- }
-
-}
+++ /dev/null
-package com.vaadin.tests.push;
-
-import org.junit.Assert;
-import org.junit.Test;
-
-import com.vaadin.testbench.elements.ButtonElement;
-import com.vaadin.tests.tb3.SingleBrowserTest;
-
-public class PushRemoveConnectorsTest extends SingleBrowserTest {
-
- @Test
- public void testNoMemoryLeak() throws InterruptedException {
- openTestURL();
- $(ButtonElement.class).id(PushRemoveConnectors.START).click();
- Thread.sleep(5000);
- int last = getMemoryUsage();
- int i = 0;
- while (i++ < 10) {
- Thread.sleep(5000);
- int now = getMemoryUsage();
- System.out.println("Memory usage: " + now);
- if (last == now)
- break;
-
- last = now;
- }
- $(ButtonElement.class).id(PushRemoveConnectors.STOP).click();
-
- Assert.assertNotEquals(10, i);
- }
-
- private int getMemoryUsage() {
- return Integer.parseInt(
- getLogRow(0).replaceFirst(".*Serialized session size: ", ""));
- }
-}