Bläddra i källkod

Refactor reconnect dialog (#11733,#17075)

* Add grace period for showing the reconnect dialog
* Try to reconnect once immediately
* Stop reconnecting when application is stopped
* Make it possible and easy to replace the reconnect dialog

Change-Id: I6695e7473859827db9dd64cbd373696aeb5d27a5
tags/7.6.0.alpha5
Artur Signell 9 år sedan
förälder
incheckning
b38dc12c24

+ 4
- 0
client/src/com/vaadin/DefaultWidgetSet.gwt.xml Visa fil

@@ -26,6 +26,10 @@
class="com.vaadin.client.metadata.ConnectorBundleLoader" />
</generate-with>

<replace-with
class="com.vaadin.client.communication.DefaultReconnectDialog">
<when-type-is class="com.vaadin.client.communication.ReconnectDialog" />
</replace-with>

<!-- Since 7.2. Compile all permutations (browser support) into one Javascript
file. Speeds up compilation and does not make the Javascript significantly

+ 85
- 0
client/src/com/vaadin/client/communication/DefaultReconnectDialog.java Visa fil

@@ -0,0 +1,85 @@
/*
* 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.client.communication;

import com.google.gwt.core.shared.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.Label;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.WidgetUtil;
import com.vaadin.client.ui.VOverlay;

/**
* The default implementation of the reconnect dialog
*
* @since 7.6
* @author Vaadin Ltd
*/
public class DefaultReconnectDialog extends VOverlay implements ReconnectDialog {
interface MyUiBinder extends UiBinder<HTMLPanel, DefaultReconnectDialog> {
}

private static MyUiBinder uiBinder = GWT.create(MyUiBinder.class);

private static final String STYLE_RECONNECTING = "active";

@UiField
public Label label;

private HandlerRegistration clickHandler = null;

public DefaultReconnectDialog() {
super(false, true);
addStyleName("v-reconnect-dialog");
setWidget(uiBinder.createAndBindUi(this));
}

@Override
public void setText(String text) {
label.setText(text);
}

@Override
public void setReconnecting(boolean reconnecting) {
setStyleName(STYLE_RECONNECTING, reconnecting);

// Click to refresh after giving up
if (!reconnecting) {
clickHandler = addDomHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
// refresh
WidgetUtil.redirect(null);
}
}, ClickEvent.getType());
} else {
if (clickHandler != null) {
clickHandler.removeHandler();
}
}
}

@Override
public void show(ApplicationConnection connection) {
setOwner(connection.getUIConnector().getWidget());
show();
}
}

client/src/com/vaadin/client/communication/ReconnectDialog.ui.xml → client/src/com/vaadin/client/communication/DefaultReconnectDialog.ui.xml Visa fil


+ 37
- 21
client/src/com/vaadin/client/communication/ReconnectDialog.java Visa fil

@@ -15,35 +15,51 @@
*/
package com.vaadin.client.communication;

import com.google.gwt.core.shared.GWT;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.Label;
import com.vaadin.client.ui.VOverlay;
import com.vaadin.client.ApplicationConnection;

/**
*
* Interface which must be implemented by the reconnect dialog
*
* @since 7.6
* @author Vaadin Ltd
*/
public class ReconnectDialog extends VOverlay {
interface MyUiBinder extends UiBinder<HTMLPanel, ReconnectDialog> {
}
public interface ReconnectDialog {

/**
* Sets the main text shown in the dialog
*
* @param text
* the text to show
*/
void setText(String text);

private static MyUiBinder uiBinder = GWT.create(MyUiBinder.class);
/**
* Sets the reconnecting state, which is true if we are trying to
* re-establish a connection with the server.
*
* @param reconnecting
* true if we are trying to re-establish the server connection,
* false if we have given up
*/
void setReconnecting(boolean reconnecting);

@UiField
public Label label;
/**
* Checks if the reconnect dialog is visible to the user
*
* @return true if the user can see the dialog, false otherwise
*/
boolean isVisible();

public ReconnectDialog() {
super(false, true);
addStyleName("v-reconnect-dialog");
setWidget(uiBinder.createAndBindUi(this));
}
/**
* Shows the dialog to the user
*
* @param connection
* the application connection this is related to
*/
void show(ApplicationConnection connection);

public void setText(String text) {
label.setText(text);
}
/**
* Hides the dialog from the user
*/
void hide();
}

+ 295
- 86
client/src/com/vaadin/client/communication/ReconnectingCommunicationProblemHandler.java Visa fil

@@ -24,6 +24,8 @@ import com.google.gwt.regexp.shared.MatchResult;
import com.google.gwt.regexp.shared.RegExp;
import com.google.gwt.user.client.Timer;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.ApplicationConnection.ApplicationStoppedEvent;
import com.vaadin.client.ApplicationConnection.ApplicationStoppedHandler;
import com.vaadin.client.WidgetUtil;
import com.vaadin.shared.ui.ui.UIState.ReconnectDialogConfigurationState;

@@ -45,21 +47,78 @@ import elemental.json.JsonObject;
public class ReconnectingCommunicationProblemHandler implements
CommunicationProblemHandler {

private static final String STYLE_RECONNECTING = "active";
private ApplicationConnection connection;
private ReconnectDialog reconnectDialog = GWT.create(ReconnectDialog.class);
private int reconnectAttempt = 0;
private Type reconnectionCause = null;

private enum Type {
HEARTBEAT, MESSAGE
private Timer scheduledReconnect;
private Timer dialogShowTimer = new Timer() {

@Override
public void run() {
showDialog();
}

};

protected enum Type {
HEARTBEAT(0), PUSH(1), XHR(2);

private int priority;

private Type(int priority) {
this.priority = priority;
}

public boolean isMessage() {
return this == PUSH || this == XHR;
}

/**
* Checks if this type is of higher priority than the given type
*
* @param type
* the type to compare to
* @return true if this type has higher priority than the given type,
* false otherwise
*/
public boolean isHigherPriorityThan(Type type) {
return priority > type.priority;
}
}

@Override
public void setConnection(ApplicationConnection connection) {
this.connection = connection;

connection.addHandler(ApplicationStoppedEvent.TYPE,
new ApplicationStoppedHandler() {
@Override
public void onApplicationStopped(
ApplicationStoppedEvent event) {
if (isReconnecting()) {
giveUp();
}
if (scheduledReconnect != null
&& scheduledReconnect.isRunning()) {
scheduledReconnect.cancel();
}
}

});
};

/**
* Checks if we are currently trying to reconnect
*
* @return true if we have noted a problem and are trying to re-establish
* server connection, false otherwise
*/
private boolean isReconnecting() {
return reconnectionCause != null;
}

private static Logger getLogger() {
return Logger.getLogger(ReconnectingCommunicationProblemHandler.class
.getName());
@@ -76,8 +135,9 @@ public class ReconnectingCommunicationProblemHandler implements

@Override
public void xhrException(CommunicationProblemEvent event) {
getLogger().warning("xhrException");
handleRecoverableError(Type.MESSAGE, event.getPayload());
debug("xhrException");
endRequest();
handleRecoverableError(Type.XHR, event.getPayload());
}

@Override
@@ -95,6 +155,10 @@ public class ReconnectingCommunicationProblemHandler implements
// Session expired
getConnection().showSessionExpiredError(null);
stopApplication();
} else if (response.getStatusCode() == Response.SC_NOT_FOUND) {
// UI closed, do nothing as the UI will react to this
// Should not trigger reconnect dialog as this will prevent user
// input
} else {
handleRecoverableError(Type.HEARTBEAT, null);
}
@@ -102,60 +166,203 @@ public class ReconnectingCommunicationProblemHandler implements

@Override
public void heartbeatOk() {
getLogger().warning("heartbeatOk");
resolveTemporaryError(Type.HEARTBEAT, true);
debug("heartbeatOk");
if (isReconnecting()) {
resolveTemporaryError(Type.HEARTBEAT);
}
}

protected void handleRecoverableError(Type type, final JsonObject payload) {
getLogger().warning("handleTemporaryError(" + type + ")");
private void debug(String msg) {
if (false) {
getLogger().warning(msg);
}
}

reconnectAttempt++;
reconnectionCause = type;
if (!reconnectDialog.isAttached()) {
reconnectDialog.setStyleName(STYLE_RECONNECTING, true);
reconnectDialog.setOwner(getConnection().getUIConnector()
.getWidget());
reconnectDialog.show();
/**
* Called whenever an error occurs in communication which should be handled
* by showing the reconnect dialog and retrying communication until
* successful again
*
* @param type
* The type of failure detected
* @param payload
* The message which did not reach the server, or null if no
* message was involved (heartbeat or push connection failed)
*/
protected void handleRecoverableError(Type type, final JsonObject payload) {
debug("handleTemporaryError(" + type + ")");
if (!connection.isApplicationRunning()) {
return;
}
if (payload != null) {
getConnection().getServerCommunicationHandler().endRequest();

if (!isReconnecting()) {
// First problem encounter
reconnectionCause = type;
getLogger().warning("Reconnecting because of " + type + " failure");
// Precaution only as there should never be a dialog at this point
// and no timer running
stopDialogTimer();
if (isDialogVisible()) {
hideDialog();
}

// Show dialog after grace period, still continue to try to
// reconnect even before it is shown
dialogShowTimer.schedule(getConfiguration().dialogGracePeriod);
} else {
// We are currently trying to reconnect
// Priority is HEARTBEAT -> PUSH -> XHR
// If a higher priority issues is resolved, we can assume the lower
// one will be also
if (type.isHigherPriorityThan(reconnectionCause)) {
getLogger().warning(
"Now reconnecting because of " + type + " failure");
reconnectionCause = type;
}
}

if (reconnectAttempt >= getConfiguration().reconnectAttempts) {
// Max attempts reached
reconnectDialog.setText(getDialogTextGaveUp(reconnectAttempt));
reconnectDialog.setStyleName(STYLE_RECONNECTING, false);
if (reconnectionCause != type) {
return;
}

getConnection().setApplicationRunning(false);
reconnectAttempt++;
getLogger().info(
"Reconnect attempt " + reconnectAttempt + " for " + type);

if (reconnectAttempt >= getConfiguration().reconnectAttempts) {
// Max attempts reached, stop trying
giveUp();
} else {
reconnectDialog.setText(getDialogText(reconnectAttempt));
updateDialog();
scheduleReconnect(payload);
}
}

// Here and not in timer to avoid TB for getting in between
if (payload != null) {
getConnection().getServerCommunicationHandler().startRequest();
}
/**
* Called after a problem occurred.
*
* This method is responsible for re-sending the payload to the server (if
* not null) or re-send a heartbeat request at some point
*
* @param payload
* the payload that did not reach the server, null if the problem
* was detected by a heartbeat
*/
protected void scheduleReconnect(final JsonObject payload) {
// Here and not in timer to avoid TB for getting in between
if (payload != null) {
getConnection().getServerCommunicationHandler().startRequest();
}

// Reconnect
new Timer() {
if (reconnectAttempt == 1) {
// Try once immediately
doReconnect(payload);
} else {
scheduledReconnect = new Timer() {
@Override
public void run() {
if (payload != null) {
getLogger().info(
"Re-sending last message to the server...");
getConnection().getServerCommunicationHandler().send(
payload);
} else {
// Use heartbeat
getLogger().info(
"Trying to re-establish server connection...");
getConnection().getHeartbeat().send();
}
scheduledReconnect = null;
doReconnect(payload);
}
}.schedule(getConfiguration().reconnectInterval);
};
scheduledReconnect.schedule(getConfiguration().reconnectInterval);
}
}

/**
* Re-sends the payload to the server (if not null) or re-sends a heartbeat
* request immediately
*
* @param payload
* the payload that did not reach the server, null if the problem
* was detected by a heartbeat
*/
protected void doReconnect(JsonObject payload) {
if (!connection.isApplicationRunning()) {
// This should not happen as nobody should call this if the
// application has been stopped
getLogger()
.warning(
"Trying to reconnect after application has been stopped. Giving up");
return;
}
if (payload != null) {
getLogger().info("Re-sending last message to the server...");
getConnection().getServerCommunicationHandler().send(payload);
} else {
// Use heartbeat
getLogger().info("Trying to re-establish server connection...");
getConnection().getHeartbeat().send();
}
}

/**
* Called whenever a reconnect attempt fails to allow updating of dialog
* contents
*/
protected void updateDialog() {
reconnectDialog.setText(getDialogText(reconnectAttempt));
}

/**
* Called when we should give up trying to reconnect and let the user decide
* how to continue
*
*/
protected void giveUp() {
stopDialogTimer();
if (!isDialogVisible()) {
// It SHOULD always be visible at this point, unless you have a
// really strange configuration (grace time longer than total
// reconnect time)
showDialog();
}
reconnectDialog.setText(getDialogTextGaveUp(reconnectAttempt));
reconnectDialog.setReconnecting(false);

// Stopping the application stops heartbeats and push
connection.setApplicationRunning(false);
}

/**
* Ensures the reconnect dialog does not popup some time from now
*/
private void stopDialogTimer() {
if (dialogShowTimer.isRunning()) {
dialogShowTimer.cancel();
}
}

/**
* Checks if the reconnect dialog is visible to the user
*
* @return true if the user can see the dialog, false otherwise
*/
protected boolean isDialogVisible() {
return reconnectDialog.isVisible();
}

/**
* Called when the reconnect dialog should be shown. This is typically when
* N seconds has passed since a problem with the connection has been
* detected
*/
protected void showDialog() {
reconnectDialog.setReconnecting(true);
reconnectDialog.show(connection);

// We never want to show loading indicator and reconnect dialog at the
// same time
connection.getLoadingIndicator().hide();
}

/**
* Called when the reconnect dialog should be hidden.
*/
protected void hideDialog() {
reconnectDialog.hide();
}

/**
* Gets the text to show in the reconnect dialog after giving up (reconnect
* limit reached)
@@ -187,7 +394,9 @@ public class ReconnectingCommunicationProblemHandler implements

@Override
public void xhrInvalidContent(CommunicationProblemEvent event) {
getLogger().warning("xhrInvalidContent");
debug("xhrInvalidContent");
endRequest();

String responseText = event.getResponse().getText();
/*
* A servlet filter or equivalent may have intercepted the request and
@@ -214,7 +423,9 @@ public class ReconnectingCommunicationProblemHandler implements

@Override
public void xhrInvalidStatusCode(CommunicationProblemEvent event) {
getLogger().warning("xhrInvalidStatusCode");
debug("xhrInvalidStatusCode");
endRequest();

Response response = event.getResponse();
int statusCode = response.getStatusCode();
getLogger().warning("Server returned " + statusCode + " for xhr");
@@ -228,22 +439,23 @@ public class ReconnectingCommunicationProblemHandler implements
// proxy between the client and the server and e.g. restart the
// server
// 5xx codes may or may not be temporary
handleRecoverableError(Type.MESSAGE, event.getPayload());
handleRecoverableError(Type.XHR, event.getPayload());
}
}

/**
* @since
*/
private void endRequest() {
getConnection().getServerCommunicationHandler().endRequest();
}

protected void handleUnauthorized(CommunicationProblemEvent event) {
/*
* Authorization has failed (401). Could be that the session has timed
* out.
*/
connection.showAuthenticationError("");
endRequestAndStopApplication();
}

private void endRequestAndStopApplication() {
connection.getServerCommunicationHandler().endRequest();

stopApplication();
}

@@ -253,10 +465,6 @@ public class ReconnectingCommunicationProblemHandler implements
connection.setApplicationRunning(false);
}

/**
* @since
* @param event
*/
private void handleUnrecoverableCommunicationError(String details,
CommunicationProblemEvent event) {
Response response = event.getResponse();
@@ -266,46 +474,38 @@ public class ReconnectingCommunicationProblemHandler implements
}
connection.handleCommunicationError(details, statusCode);

endRequestAndStopApplication();
stopApplication();

}

@Override
public void xhrOk() {
getLogger().warning("xhrOk");
resolveTemporaryError(Type.MESSAGE, true);
debug("xhrOk");
if (isReconnecting()) {
resolveTemporaryError(Type.XHR);
}
}

private void resolveTemporaryError(Type type, boolean success) {
getLogger().warning("resolveTemporaryError(" + type + ")");
private void resolveTemporaryError(Type type) {
debug("resolveTemporaryError(" + type + ")");

if (reconnectionCause == null) {
// Not trying to reconnect
return;
}
if (reconnectionCause == Type.MESSAGE && type == Type.HEARTBEAT) {
// If a heartbeat goes through while we are trying to re-send an
// XHR, we wait for the XHR to go through to avoid removing the
// reconnect dialog and then possible showing it again
if (reconnectionCause != type) {
// Waiting for some other problem to be resolved
return;
}

reconnectionCause = null;
if (reconnectDialog.isAttached()) {
reconnectDialog.hide();
}

if (success && reconnectAttempt != 0) {
getLogger().info("Re-established connection to server");
reconnectAttempt = 0;
}
hideDialog();

getLogger().info("Re-established connection to server");
}

@Override
public void pushOk(PushConnection pushConnection) {
getLogger().warning("pushOk()");
resolveTemporaryError(Type.MESSAGE, true);
debug("pushOk()");
if (isReconnecting()) {
resolveTemporaryError(Type.PUSH);
}
}

@Override
@@ -316,28 +516,37 @@ public class ReconnectingCommunicationProblemHandler implements

@Override
public void pushNotConnected(JsonObject payload) {
getLogger().warning("pushNotConnected()");
handleRecoverableError(Type.MESSAGE, payload);
debug("pushNotConnected()");
endRequest();
handleRecoverableError(Type.PUSH, payload);
}

@Override
public void pushReconnectPending(PushConnection pushConnection) {
getLogger().warning(
"pushReconnectPending(" + pushConnection.getTransportType()
+ ")");
debug("pushReconnectPending(" + pushConnection.getTransportType() + ")");
getLogger().info("Reopening push connection");
if (pushConnection.isBidirectional()) {
// Lost connection for a connection which will tell us when the
// connection is available again
handleRecoverableError(Type.PUSH, null);
} else {
// Lost connection for a connection we do not necessarily know when
// it is available again (long polling behind proxy). Do nothing and
// show reconnect dialog if the user does something and the XHR
// fails
}
}

@Override
public void pushError(PushConnection pushConnection) {
getLogger().warning("pushError()");
debug("pushError()");
connection.handleCommunicationError("Push connection using "
+ pushConnection.getTransportType() + " failed!", -1);
}

@Override
public void pushClientTimeout(PushConnection pushConnection) {
getLogger().warning("pushClientTimeout()");
debug("pushClientTimeout()");
// TODO Reconnect, allowing client timeout to be set
// https://dev.vaadin.com/ticket/18429
connection
@@ -348,7 +557,7 @@ public class ReconnectingCommunicationProblemHandler implements

@Override
public void pushClosed(PushConnection pushConnection) {
getLogger().warning("pushClosed()");
debug("pushClosed()");
getLogger().info("Push connection closed");
}


+ 1
- 2
client/src/com/vaadin/client/communication/ServerCommunicationHandler.java Visa fil

@@ -245,8 +245,7 @@ public class ServerCommunicationHandler {
}
// After sendInvocationsToServer() there may be a new active
// request, so we must set hasActiveRequest to false before, not after,
// the call. Active requests used to be tracked with an integer counter,
// so setting it after used to work but not with the #8505 changes.
// the call.
hasActiveRequest = false;

if (connection.isApplicationRunning()) {

+ 1
- 0
shared/src/com/vaadin/shared/ui/ui/UIState.java Visa fil

@@ -130,6 +130,7 @@ public class UIState extends TabIndexState {
public String dialogTextGaveUp = "Server connection lost.";
public int reconnectAttempts = 10000;
public int reconnectInterval = 5000;
public int dialogGracePeriod = 1000;
}

public static class LocaleServiceState implements Serializable {

+ 15
- 1
uitest/src/com/vaadin/tests/application/CommErrorEmulatorUI.java Visa fil

@@ -185,12 +185,26 @@ public class CommErrorEmulatorUI extends AbstractTestUIWithLog {
}
});

final TextField reconnectDialogGracePeriod = new TextField(
"Reconnect dialog grace period (ms)");
reconnectDialogGracePeriod.setConverter(Integer.class);
reconnectDialogGracePeriod
.setConvertedValue(getState().reconnectDialog.dialogGracePeriod);
reconnectDialogGracePeriod
.addValueChangeListener(new ValueChangeListener() {
@Override
public void valueChange(ValueChangeEvent event) {
getState().reconnectDialog.dialogGracePeriod = (Integer) reconnectDialogGracePeriod
.getConvertedValue();
}
});

VerticalLayout vl = new VerticalLayout();
vl.setMargin(true);
vl.setSpacing(true);
p.setContent(vl);
vl.addComponents(reconnectDialogMessage, reconnectDialogGaveUpMessage,
reconnectDialogReconnectAttempts,
reconnectDialogGracePeriod, reconnectDialogReconnectAttempts,
reconnectDialogReconnectInterval);
return p;
}

Laddar…
Avbryt
Spara