From e1b923f90623d7dcdbe355225a4049323dabacf7 Mon Sep 17 00:00:00 2001 From: Mirjan Merruko Date: Tue, 28 Mar 2017 21:25:21 +0300 Subject: Move GAEVaadinServlet to its own module --- compatibility-server-gae/pom.xml | 60 +++ .../java/com/vaadin/server/GAEVaadinServlet.java | 469 +++++++++++++++++++++ compatibility-server/bnd.bnd | 1 - compatibility-server/pom.xml | 7 - .../java/com/vaadin/server/GAEVaadinServlet.java | 469 --------------------- pom.xml | 5 + scripts/GenerateBuildTestAndStagingReport.py | 3 +- 7 files changed, 536 insertions(+), 478 deletions(-) create mode 100644 compatibility-server-gae/pom.xml create mode 100644 compatibility-server-gae/src/main/java/com/vaadin/server/GAEVaadinServlet.java delete mode 100644 compatibility-server/src/main/java/com/vaadin/server/GAEVaadinServlet.java diff --git a/compatibility-server-gae/pom.xml b/compatibility-server-gae/pom.xml new file mode 100644 index 0000000000..55b64d1013 --- /dev/null +++ b/compatibility-server-gae/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + com.vaadin + vaadin-root + 8.0-SNAPSHOT + + vaadin-compatibility-server-gae + vaadin-compatibility-server-gae + jar + + https://vaadin.com/ + Vaadin 7 GAE Servlet + + + true + + + + + ${project.groupId} + vaadin-server + ${project.version} + + + javax.servlet + javax.servlet-api + provided + + + + com.google.appengine + appengine-api-1.0-sdk + provided + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + + checkstyle + + process-sources + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + diff --git a/compatibility-server-gae/src/main/java/com/vaadin/server/GAEVaadinServlet.java b/compatibility-server-gae/src/main/java/com/vaadin/server/GAEVaadinServlet.java new file mode 100644 index 0000000000..5b25a48ba9 --- /dev/null +++ b/compatibility-server-gae/src/main/java/com/vaadin/server/GAEVaadinServlet.java @@ -0,0 +1,469 @@ +/* + * Copyright 2000-2016 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.server; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.NotSerializableException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import com.google.appengine.api.datastore.Blob; +import com.google.appengine.api.datastore.DatastoreService; +import com.google.appengine.api.datastore.DatastoreServiceFactory; +import com.google.appengine.api.datastore.Entity; +import com.google.appengine.api.datastore.EntityNotFoundException; +import com.google.appengine.api.datastore.FetchOptions.Builder; +import com.google.appengine.api.datastore.Key; +import com.google.appengine.api.datastore.KeyFactory; +import com.google.appengine.api.datastore.PreparedQuery; +import com.google.appengine.api.datastore.Query; +import com.google.appengine.api.datastore.Query.FilterOperator; +import com.google.appengine.api.memcache.Expiration; +import com.google.appengine.api.memcache.MemcacheService; +import com.google.appengine.api.memcache.MemcacheServiceFactory; +import com.google.apphosting.api.DeadlineExceededException; + +/** + * ApplicationServlet to be used when deploying to Google App Engine, in + * web.xml: + * + *
+ *      <servlet>
+ *              <servlet-name>HelloWorld</servlet-name>
+ *              <servlet-class>com.vaadin.server.GAEApplicationServlet</servlet-class>
+ *              <init-param>
+ *                      <param-name>UI</param-name>
+ *                      <param-value>com.vaadin.demo.HelloWorld</param-value>
+ *              </init-param>
+ *      </servlet>
+ * 
+ * + * Session support must be enabled in appengine-web.xml: + * + *
+ *      <sessions-enabled>true</sessions-enabled>
+ * 
+ * + * Appengine datastore cleanup can be invoked by calling one of the applications + * with an additional path "/CLEAN". This can be set up as a cron-job in + * cron.xml (see appengine documentation for more information): + * + *
+ * <cronentries>
+ *   <cron>
+ *     <url>/HelloWorld/CLEAN</url>
+ *     <description>Clean up sessions</description>
+ *     <schedule>every 2 hours</schedule>
+ *   </cron>
+ * </cronentries>
+ * 
+ * + * It is recommended (but not mandatory) to extract themes and widgetsets and + * have App Engine server these statically. Extract VAADIN folder (and it's + * contents) 'next to' the WEB-INF folder, and add the following to + * appengine-web.xml: + * + *
+ *      <static-files>
+ *           <include path="/VAADIN/**" />
+ *      </static-files>
+ * 
+ * + * Additional limitations: + * + * + * @deprecated No longer supported with Vaadin 8.0 + */ +@Deprecated +public class GAEVaadinServlet extends VaadinServlet { + + // memcache mutex is MUTEX_BASE + sessio id + private static final String MUTEX_BASE = "_vmutex"; + + // used identify ApplicationContext in memcache and datastore + private static final String AC_BASE = "_vac"; + + // UIDL requests will attempt to gain access for this long before telling + // the client to retry + private static final int MAX_UIDL_WAIT_MILLISECONDS = 5000; + + // Tell client to retry after this delay. + // Note: currently interpreting Retry-After as ms, not sec + private static final int RETRY_AFTER_MILLISECONDS = 100; + + // Properties used in the datastore + private static final String PROPERTY_EXPIRES = "expires"; + private static final String PROPERTY_DATA = "data"; + + // path used for cleanup + private static final String CLEANUP_PATH = "/CLEAN"; + // max entities to clean at once + private static final int CLEANUP_LIMIT = 200; + // appengine session kind + private static final String APPENGINE_SESSION_KIND = "_ah_SESSION"; + // appengine session expires-parameter + private static final String PROPERTY_APPENGINE_EXPIRES = "_expires"; + + // sessions with undefined (-1) expiration are limited to this, but explicit + // longer timeouts can be used + private static final int DEFAULT_MAX_INACTIVE_INTERVAL = 24 * 3600; + + protected void sendDeadlineExceededNotification( + VaadinServletRequest request, VaadinServletResponse response) + throws IOException { + criticalNotification(request, response, "Deadline Exceeded", + "I'm sorry, but the operation took too long to complete. We'll try reloading to see where we're at, please take note of any unsaved data...", + "", null); + } + + protected void sendNotSerializableNotification(VaadinServletRequest request, + VaadinServletResponse response) throws IOException { + criticalNotification(request, response, "NotSerializableException", + "I'm sorry, but there seems to be a serious problem, please contact the administrator. And please take note of any unsaved data...", + "", + getApplicationUrl(request).toString() + "?restartApplication"); + } + + protected void sendCriticalErrorNotification(VaadinServletRequest request, + VaadinServletResponse response) throws IOException { + criticalNotification(request, response, "Critical error", + "I'm sorry, but there seems to be a serious problem, please contact the administrator. And please take note of any unsaved data...", + "", + getApplicationUrl(request).toString() + "?restartApplication"); + } + + @Override + protected void service(HttpServletRequest unwrappedRequest, + HttpServletResponse unwrappedResponse) + throws ServletException, IOException { + VaadinServletRequest request = new VaadinServletRequest( + unwrappedRequest, getService()); + VaadinServletResponse response = new VaadinServletResponse( + unwrappedResponse, getService()); + + if (isCleanupRequest(request)) { + cleanDatastore(); + return; + } + + if (isStaticResourceRequest(request)) { + // no locking needed, let superclass handle + super.service(request, response); + cleanSession(request); + return; + } + + if (ServletPortletHelper.isAppRequest(request)) { + // no locking needed, let superclass handle + getApplicationContext(request, + MemcacheServiceFactory.getMemcacheService()); + super.service(request, response); + cleanSession(request); + return; + } + + final HttpSession session = request + .getSession(getService().requestCanCreateSession(request)); + if (session == null) { + try { + getService().handleSessionExpired(request, response); + } catch (ServiceException e) { + throw new ServletException(e); + } + cleanSession(request); + return; + } + + boolean locked = false; + MemcacheService memcache = null; + String mutex = MUTEX_BASE + session.getId(); + memcache = MemcacheServiceFactory.getMemcacheService(); + try { + // try to get lock + long started = System.currentTimeMillis(); + while (System.currentTimeMillis() + - started < MAX_UIDL_WAIT_MILLISECONDS) { + locked = memcache.put(mutex, 1, Expiration.byDeltaSeconds(40), + MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT); + if (locked || ServletPortletHelper.isUIDLRequest(request)) { + /* + * Done if we got a lock. Will also avoid retrying if + * there's a UIDL request because those are retried from the + * client without keeping the server thread stalled. + */ + break; + } + try { + Thread.sleep(RETRY_AFTER_MILLISECONDS); + } catch (InterruptedException e) { + getLogger() + .finer("Thread.sleep() interrupted while waiting for lock. Trying again. " + + e); + } + } + + if (!locked) { + // Not locked; only UIDL can get trough here unlocked: tell + // client to retry + response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE); + // Note: currently interpreting Retry-After as ms, not sec + response.setHeader("Retry-After", + "" + RETRY_AFTER_MILLISECONDS); + return; + } + + // de-serialize or create application context, store in session + VaadinSession ctx = getApplicationContext(request, memcache); + + super.service(request, response); + + // serialize + started = new Date().getTime(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(ctx); + oos.flush(); + byte[] bytes = baos.toByteArray(); + + started = new Date().getTime(); + + String id = AC_BASE + session.getId(); + Date expire = new Date( + started + getMaxInactiveIntervalSeconds(session) * 1000); + Expiration expires = Expiration.onDate(expire); + + memcache.put(id, bytes, expires); + + DatastoreService ds = DatastoreServiceFactory.getDatastoreService(); + Entity entity = new Entity(AC_BASE, id); + entity.setProperty(PROPERTY_EXPIRES, expire.getTime()); + entity.setProperty(PROPERTY_DATA, new Blob(bytes)); + ds.put(entity); + + } catch (DeadlineExceededException e) { + getLogger().log(Level.WARNING, "DeadlineExceeded for {0}", + session.getId()); + sendDeadlineExceededNotification(request, response); + } catch (NotSerializableException e) { + getLogger().log(Level.SEVERE, "Not serializable!", e); + + // TODO this notification is usually not shown - should we redirect + // in some other way - can we? + sendNotSerializableNotification(request, response); + } catch (Exception e) { + getLogger().log(Level.WARNING, + "An exception occurred while servicing request.", e); + + sendCriticalErrorNotification(request, response); + } finally { + // "Next, please!" + if (locked) { + memcache.delete(mutex); + } + cleanSession(request); + } + } + + /** + * Returns the maximum inactive time for a session. This is used for + * handling the expiration of session related information in caches etc. + * + * @param session + * @return inactive timeout in seconds, greater than zero + */ + protected int getMaxInactiveIntervalSeconds(final HttpSession session) { + int interval = session.getMaxInactiveInterval(); + if (interval <= 0) { + getLogger().log(Level.FINE, + "Undefined session expiration time, using default value instead."); + return DEFAULT_MAX_INACTIVE_INTERVAL; + } + return interval; + } + + protected VaadinSession getApplicationContext(HttpServletRequest request, + MemcacheService memcache) throws ServletException { + HttpSession session = request.getSession(); + String id = AC_BASE + session.getId(); + byte[] serializedAC = (byte[]) memcache.get(id); + if (serializedAC == null) { + DatastoreService ds = DatastoreServiceFactory.getDatastoreService(); + Key key = KeyFactory.createKey(AC_BASE, id); + Entity entity = null; + try { + entity = ds.get(key); + } catch (EntityNotFoundException e) { + // Ok, we were a bit optimistic; we'll create a new one later + } + if (entity != null) { + Blob blob = (Blob) entity.getProperty(PROPERTY_DATA); + serializedAC = blob.getBytes(); + // bring it to memcache + memcache.put(AC_BASE + session.getId(), serializedAC, + Expiration.byDeltaSeconds( + getMaxInactiveIntervalSeconds(session)), + MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT); + } + } + if (serializedAC != null) { + ByteArrayInputStream bais = new ByteArrayInputStream(serializedAC); + ObjectInputStream ois; + try { + ois = new ObjectInputStream(bais); + VaadinSession vaadinSession = (VaadinSession) ois.readObject(); + getService().storeSession(vaadinSession, + new WrappedHttpSession(session)); + } catch (IOException e) { + getLogger().log(Level.WARNING, + "Could not de-serialize ApplicationContext for " + + session.getId() + + " A new one will be created. ", + e); + } catch (ClassNotFoundException e) { + getLogger().log(Level.WARNING, + "Could not de-serialize ApplicationContext for " + + session.getId() + + " A new one will be created. ", + e); + } + } + + // will create new context if the above did not + try { + return getService().findVaadinSession(createVaadinRequest(request)); + } catch (Exception e) { + throw new ServletException(e); + } + } + + private boolean isCleanupRequest(HttpServletRequest request) { + String path = request.getPathInfo(); + if (path != null && path.equals(CLEANUP_PATH)) { + return true; + } + return false; + } + + /** + * Removes the ApplicationContext from the session in order to minimize the + * data serialized to datastore and memcache. + * + * @param request + */ + private void cleanSession(VaadinServletRequest request) { + // Should really be replaced with a session storage API... + WrappedSession wrappedSession = request.getWrappedSession(false); + if (wrappedSession == null) { + return; + } + VaadinSession serviceSession = getService().loadSession(wrappedSession); + if (serviceSession == null) { + return; + } + + /* + * Inform VaadinSession.valueUnbound that it should not kill the session + * even though it gets unbound. + */ + serviceSession.setAttribute( + VaadinService.PRESERVE_UNBOUND_SESSION_ATTRIBUTE, Boolean.TRUE); + getService().removeSession(serviceSession.getSession()); + + // Remove preservation marker + serviceSession.setAttribute( + VaadinService.PRESERVE_UNBOUND_SESSION_ATTRIBUTE, null); + } + + /** + * This will look at the timestamp and delete expired persisted Vaadin and + * appengine sessions from the datastore. + * + * TODO Possible improvements include: 1. Use transactions (requires entity + * groups - overkill?) 2. Delete one-at-a-time, catch possible exception, + * continue w/ next. + */ + private void cleanDatastore() { + long expire = new Date().getTime(); + try { + DatastoreService ds = DatastoreServiceFactory.getDatastoreService(); + // Vaadin stuff first + { + Query q = new Query(AC_BASE); + q.setKeysOnly(); + + q.addFilter(PROPERTY_EXPIRES, FilterOperator.LESS_THAN_OR_EQUAL, + expire); + PreparedQuery pq = ds.prepare(q); + List entities = pq + .asList(Builder.withLimit(CLEANUP_LIMIT)); + if (entities != null) { + getLogger().log(Level.INFO, + "Vaadin cleanup deleting {0} expired Vaadin sessions.", + entities.size()); + List keys = new ArrayList(); + for (Entity e : entities) { + keys.add(e.getKey()); + } + ds.delete(keys); + } + } + // Also cleanup GAE sessions + { + Query q = new Query(APPENGINE_SESSION_KIND); + q.setKeysOnly(); + q.addFilter(PROPERTY_APPENGINE_EXPIRES, + FilterOperator.LESS_THAN_OR_EQUAL, expire); + PreparedQuery pq = ds.prepare(q); + List entities = pq + .asList(Builder.withLimit(CLEANUP_LIMIT)); + if (entities != null) { + getLogger().log(Level.INFO, + "Vaadin cleanup deleting {0} expired appengine sessions.", + entities.size()); + List keys = new ArrayList(); + for (Entity e : entities) { + keys.add(e.getKey()); + } + ds.delete(keys); + } + } + } catch (Exception e) { + getLogger().log(Level.WARNING, "Exception while cleaning.", e); + } + } + + private static final Logger getLogger() { + return Logger.getLogger(GAEVaadinServlet.class.getName()); + } +} diff --git a/compatibility-server/bnd.bnd b/compatibility-server/bnd.bnd index 11ea914dcc..98aae5e56f 100644 --- a/compatibility-server/bnd.bnd +++ b/compatibility-server/bnd.bnd @@ -1,7 +1,6 @@ Bundle-SymbolicName: ${project.groupId}.compatibility.server Bundle-Name: Vaadin Compatibility Server Import-Package: com.vaadin*;version="[${osgi.bundle.version},${osgi.bundle.version}]",\ - !com.google*,\ org.osgi*,\ javax.validation*;resolution:=optional;version='${javax.validation.version}',\ * diff --git a/compatibility-server/pom.xml b/compatibility-server/pom.xml index ed3468a9a2..ab5ae30d80 100644 --- a/compatibility-server/pom.xml +++ b/compatibility-server/pom.xml @@ -59,13 +59,6 @@ provided true - - - - com.google.appengine - appengine-api-1.0-sdk - provided - diff --git a/compatibility-server/src/main/java/com/vaadin/server/GAEVaadinServlet.java b/compatibility-server/src/main/java/com/vaadin/server/GAEVaadinServlet.java deleted file mode 100644 index 5b25a48ba9..0000000000 --- a/compatibility-server/src/main/java/com/vaadin/server/GAEVaadinServlet.java +++ /dev/null @@ -1,469 +0,0 @@ -/* - * Copyright 2000-2016 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.server; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.NotSerializableException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; - -import com.google.appengine.api.datastore.Blob; -import com.google.appengine.api.datastore.DatastoreService; -import com.google.appengine.api.datastore.DatastoreServiceFactory; -import com.google.appengine.api.datastore.Entity; -import com.google.appengine.api.datastore.EntityNotFoundException; -import com.google.appengine.api.datastore.FetchOptions.Builder; -import com.google.appengine.api.datastore.Key; -import com.google.appengine.api.datastore.KeyFactory; -import com.google.appengine.api.datastore.PreparedQuery; -import com.google.appengine.api.datastore.Query; -import com.google.appengine.api.datastore.Query.FilterOperator; -import com.google.appengine.api.memcache.Expiration; -import com.google.appengine.api.memcache.MemcacheService; -import com.google.appengine.api.memcache.MemcacheServiceFactory; -import com.google.apphosting.api.DeadlineExceededException; - -/** - * ApplicationServlet to be used when deploying to Google App Engine, in - * web.xml: - * - *
- *      <servlet>
- *              <servlet-name>HelloWorld</servlet-name>
- *              <servlet-class>com.vaadin.server.GAEApplicationServlet</servlet-class>
- *              <init-param>
- *                      <param-name>UI</param-name>
- *                      <param-value>com.vaadin.demo.HelloWorld</param-value>
- *              </init-param>
- *      </servlet>
- * 
- * - * Session support must be enabled in appengine-web.xml: - * - *
- *      <sessions-enabled>true</sessions-enabled>
- * 
- * - * Appengine datastore cleanup can be invoked by calling one of the applications - * with an additional path "/CLEAN". This can be set up as a cron-job in - * cron.xml (see appengine documentation for more information): - * - *
- * <cronentries>
- *   <cron>
- *     <url>/HelloWorld/CLEAN</url>
- *     <description>Clean up sessions</description>
- *     <schedule>every 2 hours</schedule>
- *   </cron>
- * </cronentries>
- * 
- * - * It is recommended (but not mandatory) to extract themes and widgetsets and - * have App Engine server these statically. Extract VAADIN folder (and it's - * contents) 'next to' the WEB-INF folder, and add the following to - * appengine-web.xml: - * - *
- *      <static-files>
- *           <include path="/VAADIN/**" />
- *      </static-files>
- * 
- * - * Additional limitations: - *
    - *
  • Do not change application state when serving an ApplicationResource. - *
  • Avoid changing application state in transaction handlers, unless you're - * confident you fully understand the synchronization issues in App Engine. - *
  • The application remains locked while uploading - no progressbar is - * possible. - *
- * - * @deprecated No longer supported with Vaadin 8.0 - */ -@Deprecated -public class GAEVaadinServlet extends VaadinServlet { - - // memcache mutex is MUTEX_BASE + sessio id - private static final String MUTEX_BASE = "_vmutex"; - - // used identify ApplicationContext in memcache and datastore - private static final String AC_BASE = "_vac"; - - // UIDL requests will attempt to gain access for this long before telling - // the client to retry - private static final int MAX_UIDL_WAIT_MILLISECONDS = 5000; - - // Tell client to retry after this delay. - // Note: currently interpreting Retry-After as ms, not sec - private static final int RETRY_AFTER_MILLISECONDS = 100; - - // Properties used in the datastore - private static final String PROPERTY_EXPIRES = "expires"; - private static final String PROPERTY_DATA = "data"; - - // path used for cleanup - private static final String CLEANUP_PATH = "/CLEAN"; - // max entities to clean at once - private static final int CLEANUP_LIMIT = 200; - // appengine session kind - private static final String APPENGINE_SESSION_KIND = "_ah_SESSION"; - // appengine session expires-parameter - private static final String PROPERTY_APPENGINE_EXPIRES = "_expires"; - - // sessions with undefined (-1) expiration are limited to this, but explicit - // longer timeouts can be used - private static final int DEFAULT_MAX_INACTIVE_INTERVAL = 24 * 3600; - - protected void sendDeadlineExceededNotification( - VaadinServletRequest request, VaadinServletResponse response) - throws IOException { - criticalNotification(request, response, "Deadline Exceeded", - "I'm sorry, but the operation took too long to complete. We'll try reloading to see where we're at, please take note of any unsaved data...", - "", null); - } - - protected void sendNotSerializableNotification(VaadinServletRequest request, - VaadinServletResponse response) throws IOException { - criticalNotification(request, response, "NotSerializableException", - "I'm sorry, but there seems to be a serious problem, please contact the administrator. And please take note of any unsaved data...", - "", - getApplicationUrl(request).toString() + "?restartApplication"); - } - - protected void sendCriticalErrorNotification(VaadinServletRequest request, - VaadinServletResponse response) throws IOException { - criticalNotification(request, response, "Critical error", - "I'm sorry, but there seems to be a serious problem, please contact the administrator. And please take note of any unsaved data...", - "", - getApplicationUrl(request).toString() + "?restartApplication"); - } - - @Override - protected void service(HttpServletRequest unwrappedRequest, - HttpServletResponse unwrappedResponse) - throws ServletException, IOException { - VaadinServletRequest request = new VaadinServletRequest( - unwrappedRequest, getService()); - VaadinServletResponse response = new VaadinServletResponse( - unwrappedResponse, getService()); - - if (isCleanupRequest(request)) { - cleanDatastore(); - return; - } - - if (isStaticResourceRequest(request)) { - // no locking needed, let superclass handle - super.service(request, response); - cleanSession(request); - return; - } - - if (ServletPortletHelper.isAppRequest(request)) { - // no locking needed, let superclass handle - getApplicationContext(request, - MemcacheServiceFactory.getMemcacheService()); - super.service(request, response); - cleanSession(request); - return; - } - - final HttpSession session = request - .getSession(getService().requestCanCreateSession(request)); - if (session == null) { - try { - getService().handleSessionExpired(request, response); - } catch (ServiceException e) { - throw new ServletException(e); - } - cleanSession(request); - return; - } - - boolean locked = false; - MemcacheService memcache = null; - String mutex = MUTEX_BASE + session.getId(); - memcache = MemcacheServiceFactory.getMemcacheService(); - try { - // try to get lock - long started = System.currentTimeMillis(); - while (System.currentTimeMillis() - - started < MAX_UIDL_WAIT_MILLISECONDS) { - locked = memcache.put(mutex, 1, Expiration.byDeltaSeconds(40), - MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT); - if (locked || ServletPortletHelper.isUIDLRequest(request)) { - /* - * Done if we got a lock. Will also avoid retrying if - * there's a UIDL request because those are retried from the - * client without keeping the server thread stalled. - */ - break; - } - try { - Thread.sleep(RETRY_AFTER_MILLISECONDS); - } catch (InterruptedException e) { - getLogger() - .finer("Thread.sleep() interrupted while waiting for lock. Trying again. " - + e); - } - } - - if (!locked) { - // Not locked; only UIDL can get trough here unlocked: tell - // client to retry - response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE); - // Note: currently interpreting Retry-After as ms, not sec - response.setHeader("Retry-After", - "" + RETRY_AFTER_MILLISECONDS); - return; - } - - // de-serialize or create application context, store in session - VaadinSession ctx = getApplicationContext(request, memcache); - - super.service(request, response); - - // serialize - started = new Date().getTime(); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - ObjectOutputStream oos = new ObjectOutputStream(baos); - oos.writeObject(ctx); - oos.flush(); - byte[] bytes = baos.toByteArray(); - - started = new Date().getTime(); - - String id = AC_BASE + session.getId(); - Date expire = new Date( - started + getMaxInactiveIntervalSeconds(session) * 1000); - Expiration expires = Expiration.onDate(expire); - - memcache.put(id, bytes, expires); - - DatastoreService ds = DatastoreServiceFactory.getDatastoreService(); - Entity entity = new Entity(AC_BASE, id); - entity.setProperty(PROPERTY_EXPIRES, expire.getTime()); - entity.setProperty(PROPERTY_DATA, new Blob(bytes)); - ds.put(entity); - - } catch (DeadlineExceededException e) { - getLogger().log(Level.WARNING, "DeadlineExceeded for {0}", - session.getId()); - sendDeadlineExceededNotification(request, response); - } catch (NotSerializableException e) { - getLogger().log(Level.SEVERE, "Not serializable!", e); - - // TODO this notification is usually not shown - should we redirect - // in some other way - can we? - sendNotSerializableNotification(request, response); - } catch (Exception e) { - getLogger().log(Level.WARNING, - "An exception occurred while servicing request.", e); - - sendCriticalErrorNotification(request, response); - } finally { - // "Next, please!" - if (locked) { - memcache.delete(mutex); - } - cleanSession(request); - } - } - - /** - * Returns the maximum inactive time for a session. This is used for - * handling the expiration of session related information in caches etc. - * - * @param session - * @return inactive timeout in seconds, greater than zero - */ - protected int getMaxInactiveIntervalSeconds(final HttpSession session) { - int interval = session.getMaxInactiveInterval(); - if (interval <= 0) { - getLogger().log(Level.FINE, - "Undefined session expiration time, using default value instead."); - return DEFAULT_MAX_INACTIVE_INTERVAL; - } - return interval; - } - - protected VaadinSession getApplicationContext(HttpServletRequest request, - MemcacheService memcache) throws ServletException { - HttpSession session = request.getSession(); - String id = AC_BASE + session.getId(); - byte[] serializedAC = (byte[]) memcache.get(id); - if (serializedAC == null) { - DatastoreService ds = DatastoreServiceFactory.getDatastoreService(); - Key key = KeyFactory.createKey(AC_BASE, id); - Entity entity = null; - try { - entity = ds.get(key); - } catch (EntityNotFoundException e) { - // Ok, we were a bit optimistic; we'll create a new one later - } - if (entity != null) { - Blob blob = (Blob) entity.getProperty(PROPERTY_DATA); - serializedAC = blob.getBytes(); - // bring it to memcache - memcache.put(AC_BASE + session.getId(), serializedAC, - Expiration.byDeltaSeconds( - getMaxInactiveIntervalSeconds(session)), - MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT); - } - } - if (serializedAC != null) { - ByteArrayInputStream bais = new ByteArrayInputStream(serializedAC); - ObjectInputStream ois; - try { - ois = new ObjectInputStream(bais); - VaadinSession vaadinSession = (VaadinSession) ois.readObject(); - getService().storeSession(vaadinSession, - new WrappedHttpSession(session)); - } catch (IOException e) { - getLogger().log(Level.WARNING, - "Could not de-serialize ApplicationContext for " - + session.getId() - + " A new one will be created. ", - e); - } catch (ClassNotFoundException e) { - getLogger().log(Level.WARNING, - "Could not de-serialize ApplicationContext for " - + session.getId() - + " A new one will be created. ", - e); - } - } - - // will create new context if the above did not - try { - return getService().findVaadinSession(createVaadinRequest(request)); - } catch (Exception e) { - throw new ServletException(e); - } - } - - private boolean isCleanupRequest(HttpServletRequest request) { - String path = request.getPathInfo(); - if (path != null && path.equals(CLEANUP_PATH)) { - return true; - } - return false; - } - - /** - * Removes the ApplicationContext from the session in order to minimize the - * data serialized to datastore and memcache. - * - * @param request - */ - private void cleanSession(VaadinServletRequest request) { - // Should really be replaced with a session storage API... - WrappedSession wrappedSession = request.getWrappedSession(false); - if (wrappedSession == null) { - return; - } - VaadinSession serviceSession = getService().loadSession(wrappedSession); - if (serviceSession == null) { - return; - } - - /* - * Inform VaadinSession.valueUnbound that it should not kill the session - * even though it gets unbound. - */ - serviceSession.setAttribute( - VaadinService.PRESERVE_UNBOUND_SESSION_ATTRIBUTE, Boolean.TRUE); - getService().removeSession(serviceSession.getSession()); - - // Remove preservation marker - serviceSession.setAttribute( - VaadinService.PRESERVE_UNBOUND_SESSION_ATTRIBUTE, null); - } - - /** - * This will look at the timestamp and delete expired persisted Vaadin and - * appengine sessions from the datastore. - * - * TODO Possible improvements include: 1. Use transactions (requires entity - * groups - overkill?) 2. Delete one-at-a-time, catch possible exception, - * continue w/ next. - */ - private void cleanDatastore() { - long expire = new Date().getTime(); - try { - DatastoreService ds = DatastoreServiceFactory.getDatastoreService(); - // Vaadin stuff first - { - Query q = new Query(AC_BASE); - q.setKeysOnly(); - - q.addFilter(PROPERTY_EXPIRES, FilterOperator.LESS_THAN_OR_EQUAL, - expire); - PreparedQuery pq = ds.prepare(q); - List entities = pq - .asList(Builder.withLimit(CLEANUP_LIMIT)); - if (entities != null) { - getLogger().log(Level.INFO, - "Vaadin cleanup deleting {0} expired Vaadin sessions.", - entities.size()); - List keys = new ArrayList(); - for (Entity e : entities) { - keys.add(e.getKey()); - } - ds.delete(keys); - } - } - // Also cleanup GAE sessions - { - Query q = new Query(APPENGINE_SESSION_KIND); - q.setKeysOnly(); - q.addFilter(PROPERTY_APPENGINE_EXPIRES, - FilterOperator.LESS_THAN_OR_EQUAL, expire); - PreparedQuery pq = ds.prepare(q); - List entities = pq - .asList(Builder.withLimit(CLEANUP_LIMIT)); - if (entities != null) { - getLogger().log(Level.INFO, - "Vaadin cleanup deleting {0} expired appengine sessions.", - entities.size()); - List keys = new ArrayList(); - for (Entity e : entities) { - keys.add(e.getKey()); - } - ds.delete(keys); - } - } - } catch (Exception e) { - getLogger().log(Level.WARNING, "Exception while cleaning.", e); - } - } - - private static final Logger getLogger() { - return Logger.getLogger(GAEVaadinServlet.class.getName()); - } -} diff --git a/pom.xml b/pom.xml index a26d7b0801..79577d2539 100644 --- a/pom.xml +++ b/pom.xml @@ -620,6 +620,7 @@ liferay all compatibility-server + compatibility-server-gae compatibility-client compatibility-client-compiled compatibility-shared @@ -662,6 +663,7 @@ client-compiled themes compatibility-server + compatibility-server-gae compatibility-client compatibility-client-compiled compatibility-shared @@ -726,6 +728,7 @@ client-compiled themes compatibility-server + compatibility-server-gae compatibility-client compatibility-client-compiled compatibility-shared @@ -790,6 +793,7 @@ client-compiled themes compatibility-server + compatibility-server-gae compatibility-client compatibility-client-compiled compatibility-shared @@ -848,6 +852,7 @@ push client compatibility-server + compatibility-server-gae compatibility-client client-compiler shared diff --git a/scripts/GenerateBuildTestAndStagingReport.py b/scripts/GenerateBuildTestAndStagingReport.py index 94e5edc3a7..5aeb7f3f98 100644 --- a/scripts/GenerateBuildTestAndStagingReport.py +++ b/scripts/GenerateBuildTestAndStagingReport.py @@ -71,6 +71,7 @@ def getApiDiffHtml(): "client", "client-compiler", "compatibility-client", "compatibility-server", + "compatibility-server-gae", "compatibility-shared", "server", "shared" ] @@ -121,7 +122,7 @@ def completeArtifactNames(artifactIds, version): return list(map(lambda x: completeArtifactName(x, version), artifactIds)) -allowedArtifacts = completeArtifactNames([ 'vaadin-maven-plugin', 'vaadin-archetypes', 'vaadin-archetype-application', 'vaadin-archetype-application-multimodule', 'vaadin-archetype-application-example', 'vaadin-archetype-widget', 'vaadin-archetype-liferay-portlet', 'vaadin-root', 'vaadin-shared', 'vaadin-server', 'vaadin-client', 'vaadin-client-compiler', 'vaadin-client-compiled', 'vaadin-push', 'vaadin-themes', 'vaadin-compatibility-shared', 'vaadin-compatibility-server', 'vaadin-compatibility-client', 'vaadin-compatibility-client-compiled', 'vaadin-compatibility-themes', 'vaadin-testbench-api', 'vaadin-bom' ], args.version) +allowedArtifacts = completeArtifactNames([ 'vaadin-maven-plugin', 'vaadin-archetypes', 'vaadin-archetype-application', 'vaadin-archetype-application-multimodule', 'vaadin-archetype-application-example', 'vaadin-archetype-widget', 'vaadin-archetype-liferay-portlet', 'vaadin-root', 'vaadin-shared', 'vaadin-server', 'vaadin-client', 'vaadin-client-compiler', 'vaadin-client-compiled', 'vaadin-push', 'vaadin-themes', 'vaadin-compatibility-shared', 'vaadin-compatibility-server', "vaadin-compatibility-server-gae", 'vaadin-compatibility-client', 'vaadin-compatibility-client-compiled', 'vaadin-compatibility-themes', 'vaadin-testbench-api', 'vaadin-bom' ], args.version) content = "" traffic_light = "" -- cgit v1.2.3