diff options
Diffstat (limited to 'server/src/com/vaadin/server/GAEVaadinServlet.java')
-rw-r--r-- | server/src/com/vaadin/server/GAEVaadinServlet.java | 428 |
1 files changed, 428 insertions, 0 deletions
diff --git a/server/src/com/vaadin/server/GAEVaadinServlet.java b/server/src/com/vaadin/server/GAEVaadinServlet.java new file mode 100644 index 0000000000..642737f73b --- /dev/null +++ b/server/src/com/vaadin/server/GAEVaadinServlet.java @@ -0,0 +1,428 @@ +/* + * 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 + * 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: + * + * <pre> + * <servlet> + * <servlet-name>HelloWorld</servlet-name> + * <servlet-class>com.vaadin.server.GAEApplicationServlet</servlet-class> + * <init-param> + * <param-name>application</param-name> + * <param-value>com.vaadin.demo.HelloWorld</param-value> + * </init-param> + * </servlet> + * </pre> + * + * Session support must be enabled in appengine-web.xml: + * + * <pre> + * <sessions-enabled>true</sessions-enabled> + * </pre> + * + * 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): + * + * <pre> + * <cronentries> + * <cron> + * <url>/HelloWorld/CLEAN</url> + * <description>Clean up sessions</description> + * <schedule>every 2 hours</schedule> + * </cron> + * </cronentries> + * </pre> + * + * 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: + * + * <pre> + * <static-files> + * <include path="/VAADIN/**" /> + * </static-files> + * </pre> + * + * Additional limitations: + * <ul> + * <li/>Do not change application state when serving an ApplicationResource. + * <li/>Avoid changing application state in transaction handlers, unless you're + * confident you fully understand the synchronization issues in App Engine. + * <li/>The application remains locked while uploading - no progressbar is + * possible. + * </ul> + */ +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"; + + protected void sendDeadlineExceededNotification( + WrappedHttpServletRequest request, + WrappedHttpServletResponse 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( + WrappedHttpServletRequest request, + WrappedHttpServletResponse 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( + WrappedHttpServletRequest request, + WrappedHttpServletResponse 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 { + WrappedHttpServletRequest request = new WrappedHttpServletRequest( + unwrappedRequest, getDeploymentConfiguration()); + WrappedHttpServletResponse response = new WrappedHttpServletResponse( + unwrappedResponse, getDeploymentConfiguration()); + + if (isCleanupRequest(request)) { + cleanDatastore(); + return; + } + + RequestType requestType = getRequestType(request); + + if (requestType == RequestType.STATIC_FILE) { + // no locking needed, let superclass handle + super.service(request, response); + cleanSession(request); + return; + } + + if (requestType == RequestType.APPLICATION_RESOURCE) { + // no locking needed, let superclass handle + getApplicationContext(request, + MemcacheServiceFactory.getMemcacheService()); + super.service(request, response); + cleanSession(request); + return; + } + + final HttpSession session = request + .getSession(requestCanCreateApplication(request, requestType)); + if (session == null) { + handleServiceSessionExpired(request, response); + 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 = new Date().getTime(); + // non-UIDL requests will try indefinitely + while (requestType != RequestType.UIDL + || new Date().getTime() - started < MAX_UIDL_WAIT_MILLISECONDS) { + locked = memcache.put(mutex, 1, Expiration.byDeltaSeconds(40), + MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT); + if (locked) { + 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 + ApplicationContext 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 + + (session.getMaxInactiveInterval() * 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().warning("DeadlineExceeded for " + 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); + } + } + + protected ApplicationContext getApplicationContext( + HttpServletRequest request, MemcacheService memcache) { + 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(session + .getMaxInactiveInterval()), + MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT); + } + } + if (serializedAC != null) { + ByteArrayInputStream bais = new ByteArrayInputStream(serializedAC); + ObjectInputStream ois; + try { + ois = new ObjectInputStream(bais); + ApplicationContext applicationContext = (ApplicationContext) ois + .readObject(); + session.setAttribute(ServletApplicationContext.class.getName(), + applicationContext); + } 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 + return getApplicationContext(session); + + } + + private boolean isCleanupRequest(HttpServletRequest request) { + String path = getRequestPathInfo(request); + 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(HttpServletRequest request) { + HttpSession session = request.getSession(false); + if (session != null) { + session.removeAttribute(ServletApplicationContext.class.getName()); + } + } + + /** + * 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<Entity> entities = pq.asList(Builder + .withLimit(CLEANUP_LIMIT)); + if (entities != null) { + getLogger().info( + "Vaadin cleanup deleting " + entities.size() + + " expired Vaadin sessions."); + List<Key> keys = new ArrayList<Key>(); + 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<Entity> entities = pq.asList(Builder + .withLimit(CLEANUP_LIMIT)); + if (entities != null) { + getLogger().info( + "Vaadin cleanup deleting " + entities.size() + + " expired appengine sessions."); + List<Key> keys = new ArrayList<Key>(); + 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()); + } +} |