]> source.dussan.org Git - vaadin-framework.git/commitdiff
Sending and receiving heartbeat requests (#9265)
authorJohannes Dahlström <johannesd@vaadin.com>
Wed, 22 Aug 2012 10:59:26 +0000 (13:59 +0300)
committerJohannes Dahlström <johannesd@vaadin.com>
Wed, 22 Aug 2012 11:30:53 +0000 (14:30 +0300)
client/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java
client/src/com/vaadin/terminal/gwt/client/ui/root/RootConnector.java
server/src/com/vaadin/terminal/gwt/server/AbstractApplicationPortlet.java
server/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java
server/src/com/vaadin/terminal/gwt/server/AbstractCommunicationManager.java
server/src/com/vaadin/terminal/gwt/server/ServletPortletHelper.java
shared/src/com/vaadin/shared/ApplicationConstants.java

index a8852fe9fab46ee89b249fe2d09ae5adb6727697..d2deb701905f96af92a87158020788248d8a53cd 100644 (file)
@@ -230,6 +230,8 @@ public class ApplicationConnection {
 
         rootConnector.init(cnf.getRootPanelId(), this);
         showLoadingIndicator();
+
+        scheduleHeartbeat();
     }
 
     /**
@@ -2520,4 +2522,72 @@ public class ApplicationConnection {
     public SerializerMap getSerializerMap() {
         return serializerMap;
     }
+
+    /**
+     * Schedules a heartbeat request.
+     * 
+     * @see #sendHeartbeat()
+     */
+    private void scheduleHeartbeat() {
+        final int interval = 1000 * getConfiguration().getHeartbeatInterval();
+        if (interval > 0) {
+            new Timer() {
+                @Override
+                public void run() {
+                    sendHeartbeat();
+                }
+            }.schedule(interval);
+        }
+    }
+
+    /**
+     * Sends a heartbeat request to the server.
+     * <p>
+     * Heartbeat requests are used to inform the server that the client-side is
+     * still alive. If the client page is closed or the connection lost, the
+     * server will eventually close the inactive Root.
+     * <p>
+     * <b>TODO</b>: Improved error handling, like in doUidlRequest().
+     * 
+     * @see #scheduleHeartbeat()
+     * @see com.vaadin.ui.Root#heartbeat()
+     */
+    private void sendHeartbeat() {
+        final String uri = addGetParameters(
+                translateVaadinUri(ApplicationConstants.APP_PROTOCOL_PREFIX
+                        + ApplicationConstants.HEARTBEAT_REQUEST_PATH),
+                ApplicationConstants.ROOT_ID_PARAMETER + "="
+                        + getConfiguration().getRootId());
+
+        final RequestBuilder rb = new RequestBuilder(RequestBuilder.POST, uri);
+
+        final RequestCallback callback = new RequestCallback() {
+
+            @Override
+            public void onResponseReceived(Request request, Response response) {
+                int status = response.getStatusCode();
+                if (status == Response.SC_OK) {
+                    // TODO Permit retry in some error situations
+                    scheduleHeartbeat();
+                } else {
+                    VConsole.error("Heartbeat request failed with status code "
+                            + status);
+                }
+            }
+
+            @Override
+            public void onError(Request request, Throwable exception) {
+                VConsole.error("Heartbeat request resulted in exception");
+                VConsole.error(exception);
+            }
+        };
+
+        rb.setCallback(callback);
+
+        try {
+            rb.send();
+        } catch (RequestException re) {
+            callback.onError(null, re);
+        }
+    }
 }
index 0a0e0e50824293363d1106cb7694b4fda161ed73..39702e6ba0ba30675497b418c28205ecce5479b2 100644 (file)
@@ -23,16 +23,10 @@ import com.google.gwt.core.client.Scheduler;
 import com.google.gwt.dom.client.NativeEvent;
 import com.google.gwt.dom.client.Style;
 import com.google.gwt.dom.client.Style.Position;
-import com.google.gwt.http.client.Request;
-import com.google.gwt.http.client.RequestBuilder;
-import com.google.gwt.http.client.RequestCallback;
-import com.google.gwt.http.client.RequestException;
-import com.google.gwt.http.client.Response;
 import com.google.gwt.user.client.Command;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Event;
 import com.google.gwt.user.client.History;
-import com.google.gwt.user.client.Timer;
 import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.ui.RootPanel;
 import com.google.gwt.user.client.ui.Widget;
@@ -90,14 +84,6 @@ public class RootConnector extends AbstractComponentContainerConnector
                 com.google.gwt.user.client.Window.setTitle(title);
             }
         });
-        final int heartbeatInterval = getState().getHeartbeatInterval();
-        new Timer() {
-            @Override
-            public void run() {
-                sendHeartbeat();
-                schedule(heartbeatInterval);
-            }
-        }.schedule(heartbeatInterval);
     }
 
     @Override
@@ -455,29 +441,4 @@ public class RootConnector extends AbstractComponentContainerConnector
             }
         });
     }
-
-    private void sendHeartbeat() {
-        RequestBuilder rb = new RequestBuilder(RequestBuilder.POST, "url");
-
-        rb.setCallback(new RequestCallback() {
-
-            @Override
-            public void onResponseReceived(Request request, Response response) {
-                // TODO Auto-generated method stub
-
-            }
-
-            @Override
-            public void onError(Request request, Throwable exception) {
-                // TODO Auto-generated method stub
-
-            }
-        });
-
-        try {
-            rb.send();
-        } catch (RequestException re) {
-
-        }
-    }
 }
index bd39504237be62d5a60951e3589987fc386d1a59..dea42b6b69292877ea1223ddd926f3da36c4cdd8 100644 (file)
@@ -340,7 +340,7 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet
     }
 
     protected enum RequestType {
-        FILE_UPLOAD, UIDL, RENDER, STATIC_FILE, APPLICATION_RESOURCE, DUMMY, EVENT, ACTION, UNKNOWN, BROWSER_DETAILS, CONNECTOR_RESOURCE;
+        FILE_UPLOAD, UIDL, RENDER, STATIC_FILE, APPLICATION_RESOURCE, DUMMY, EVENT, ACTION, UNKNOWN, BROWSER_DETAILS, CONNECTOR_RESOURCE, HEARTBEAT;
     }
 
     protected RequestType getRequestType(WrappedPortletRequest wrappedRequest) {
@@ -361,6 +361,8 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet
             } else if (ServletPortletHelper
                     .isApplicationResourceRequest(wrappedRequest)) {
                 return RequestType.APPLICATION_RESOURCE;
+            } else if (ServletPortletHelper.isHeartbeatRequest(wrappedRequest)) {
+                return RequestType.HEARTBEAT;
             } else if (isDummyRequest(resourceRequest)) {
                 return RequestType.DUMMY;
             } else {
@@ -431,6 +433,7 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet
             Application application = null;
             boolean transactionStarted = false;
             boolean requestStarted = false;
+            boolean applicationRunning = false;
 
             try {
                 // TODO What about PARAM_UNLOADBURST & redirectToApplication??
@@ -459,6 +462,10 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet
                     applicationManager.serveConnectorResource(wrappedRequest,
                             wrappedResponse);
                     return;
+                } else if (requestType == RequestType.HEARTBEAT) {
+                    applicationManager.handleHeartbeatRequest(wrappedRequest,
+                            wrappedResponse, application);
+                    return;
                 }
 
                 /* Update browser information from request */
@@ -477,6 +484,7 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet
 
                 /* Start the newly created application */
                 startApplication(request, application, applicationContext);
+                applicationRunning = true;
 
                 /*
                  * Transaction starts. Call transaction listeners. Transaction
@@ -585,6 +593,11 @@ public abstract class AbstractApplicationPortlet extends GenericPortlet
                 handleServiceException(wrappedRequest, wrappedResponse,
                         application, e);
             } finally {
+
+                if (applicationRunning) {
+                    application.closeInactiveRoots();
+                }
+
                 // Notifies transaction end
                 try {
                     if (transactionStarted) {
index 062ba6cdf7c93c897695e8eaf84ba78f2931697d..0fc8bc09a054642e3bc915ca9ed14d3e297f2599 100644 (file)
@@ -248,6 +248,7 @@ public abstract class AbstractApplicationServlet extends HttpServlet implements
         Application application = null;
         boolean transactionStarted = false;
         boolean requestStarted = false;
+        boolean applicationRunning = false;
 
         try {
             // If a duplicate "close application" URL is received for an
@@ -287,6 +288,10 @@ public abstract class AbstractApplicationServlet extends HttpServlet implements
             if (requestType == RequestType.CONNECTOR_RESOURCE) {
                 applicationManager.serveConnectorResource(request, response);
                 return;
+            } else if (requestType == RequestType.HEARTBEAT) {
+                applicationManager.handleHeartbeatRequest(request, response,
+                        application);
+                return;
             }
 
             /* Update browser information from the request */
@@ -304,6 +309,7 @@ public abstract class AbstractApplicationServlet extends HttpServlet implements
 
             // Start the application if it's newly created
             startApplication(request, application, webApplicationContext);
+            applicationRunning = true;
 
             /*
              * Transaction starts. Call transaction listeners. Transaction end
@@ -354,6 +360,11 @@ public abstract class AbstractApplicationServlet extends HttpServlet implements
         } catch (final Throwable e) {
             handleServiceException(request, response, application, e);
         } finally {
+
+            if (applicationRunning) {
+                application.closeInactiveRoots();
+            }
+
             // Notifies transaction end
             try {
                 if (transactionStarted) {
@@ -1121,7 +1132,7 @@ public abstract class AbstractApplicationServlet extends HttpServlet implements
     }
 
     protected enum RequestType {
-        FILE_UPLOAD, BROWSER_DETAILS, UIDL, OTHER, STATIC_FILE, APPLICATION_RESOURCE, CONNECTOR_RESOURCE;
+        FILE_UPLOAD, BROWSER_DETAILS, UIDL, OTHER, STATIC_FILE, APPLICATION_RESOURCE, CONNECTOR_RESOURCE, HEARTBEAT;
     }
 
     protected RequestType getRequestType(WrappedHttpServletRequest request) {
@@ -1137,6 +1148,8 @@ public abstract class AbstractApplicationServlet extends HttpServlet implements
             return RequestType.STATIC_FILE;
         } else if (ServletPortletHelper.isApplicationResourceRequest(request)) {
             return RequestType.APPLICATION_RESOURCE;
+        } else if (ServletPortletHelper.isHeartbeatRequest(request)) {
+            return RequestType.HEARTBEAT;
         }
         return RequestType.OTHER;
 
index 00e65382cd330494d6b6bbfc392a3d2285306b9b..30999034543f92c5d1b25a554a525f4bfdda7adf 100644 (file)
@@ -87,6 +87,7 @@ import com.vaadin.terminal.Vaadin6Component;
 import com.vaadin.terminal.VariableOwner;
 import com.vaadin.terminal.WrappedRequest;
 import com.vaadin.terminal.WrappedResponse;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
 import com.vaadin.terminal.gwt.server.BootstrapHandler.BootstrapContext;
 import com.vaadin.terminal.gwt.server.ComponentSizeValidator.InvalidLayout;
 import com.vaadin.terminal.gwt.server.RpcManager.RpcInvocationException;
@@ -102,7 +103,7 @@ import com.vaadin.ui.Window;
  * This is a common base class for the server-side implementations of the
  * communication system between the client code (compiled with GWT into
  * JavaScript) and the server side components. Its client side counterpart is
- * {@link ApplicationConstants}.
+ * {@link ApplicationConnection}.
  * 
  * TODO Document better!
  */
@@ -577,6 +578,9 @@ public abstract class AbstractCommunicationManager implements Serializable {
                 return;
             }
 
+            // Keep the root alive
+            root.heartbeat();
+
             // Change all variables based on request parameters
             if (!handleVariables(request, response, callback, application, root)) {
 
@@ -2634,6 +2638,38 @@ public abstract class AbstractCommunicationManager implements Serializable {
 
     }
 
+    /**
+     * Handles a heartbeat request. Heartbeat requests are periodically sent by
+     * the client-side to inform the server that the root sending the heartbeat
+     * is still alive (the browser window is open, the connection is up) even
+     * when there are no UIDL requests for a prolonged period of time. Roots
+     * that do not receive either heartbeat or UIDL requests are eventually
+     * removed from the application and garbage collected.
+     * 
+     * @param request
+     * @param response
+     * @param application
+     * @throws IOException
+     */
+    public void handleHeartbeatRequest(WrappedRequest request,
+            WrappedResponse response, Application application)
+            throws IOException {
+        Root root = null;
+        try {
+            int rootId = Integer.parseInt(request
+                    .getParameter(ApplicationConstants.ROOT_ID_PARAMETER));
+            root = application.getRootById(rootId);
+        } catch (NumberFormatException nfe) {
+            // null-check below handles this as well
+        }
+        if (root != null) {
+            root.heartbeat();
+        } else {
+            response.sendError(HttpServletResponse.SC_NOT_FOUND,
+                    "Root not found");
+        }
+    }
+
     public StreamVariable getStreamVariable(String connectorId,
             String variableName) {
         Map<String, StreamVariable> map = pidToNameToStreamVariable
index 200f9a91030aeba1dfd35a9a37616ab4c5f3fbf4..69111019209767721564aecde176c4a38d90a374 100644 (file)
@@ -9,7 +9,7 @@ import com.vaadin.terminal.WrappedRequest;
 import com.vaadin.ui.Root;
 
 /*
 * Copyright 2011 Vaadin Ltd.
+ * Copyright 2011 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
@@ -129,4 +129,9 @@ class ServletPortletHelper implements Serializable {
         return hasPathPrefix(request, ApplicationConstants.APP_REQUEST_PATH);
     }
 
+    public static boolean isHeartbeatRequest(WrappedRequest request) {
+        return hasPathPrefix(request,
+                ApplicationConstants.HEARTBEAT_REQUEST_PATH);
+    }
+
 }
index 31e633a210418bf615b61d422147a2a69505294b..9d1ed7341d40a59fa9b9efe3ec1c80c70989bbec 100644 (file)
@@ -23,6 +23,8 @@ public class ApplicationConstants {
 
     public static final String UIDL_REQUEST_PATH = "UIDL/";
 
+    public static final String HEARTBEAT_REQUEST_PATH = "HEARTBEAT/";
+
     public static final String CONNECTOR_RESOURCE_PREFIX = APP_REQUEST_PATH
             + "CONNECTOR";