All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vaadin.server.GAEVaadinServlet Maven / Gradle / Ivy

There is a newer version: 8.27.3
Show newest version
/*
 * Vaadin Framework 7
 *
 * Copyright (C) 2000-2024 Vaadin Ltd
 *
 * This program is available under Vaadin Commercial License and Service Terms.
 *
 * See  for the full
 * 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. *
*/ 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()); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy