|
|
@@ -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"); |
|
|
|
} |
|
|
|
|