From a7aca3444a898e26a417fab1f99f122e544be0f9 Mon Sep 17 00:00:00 2001 From: Marc Englund Date: Fri, 4 Sep 2009 12:38:51 +0000 Subject: [PATCH] Google appengine support w/ own 'session handling' and synchronization. For #3058 and #2835 svn changeset:8669/svn branch:6.1 --- .../server/AbstractApplicationServlet.java | 55 ++++- .../gwt/server/GAEApplicationServlet.java | 205 ++++++++++++++---- .../vaadin/tests/appengine/GAESyncTest.java | 2 +- 3 files changed, 214 insertions(+), 48 deletions(-) diff --git a/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java b/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java index af1847a7b7..31d5d5ac96 100644 --- a/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java +++ b/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java @@ -361,13 +361,14 @@ public abstract class AbstractApplicationServlet extends HttpServlet { protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - // check if we should serve static files (widgetsets, themes) - if (serveStaticResources(request, response)) { + RequestType requestType = getRequestType(request); + + if (requestType == RequestType.STATIC_FILE) { + serveStaticResources(request, response); return; } Application application = null; - RequestType requestType = getRequestType(request); try { // If a duplicate "close application" URL is received for an @@ -638,7 +639,7 @@ public abstract class AbstractApplicationServlet extends HttpServlet { * @param requestType * @return true if an application should be created, false otherwise */ - private boolean requestCanCreateApplication(HttpServletRequest request, + boolean requestCanCreateApplication(HttpServletRequest request, RequestType requestType) { if (requestType == RequestType.UIDL && isRepaintAll(request)) { /* @@ -650,8 +651,8 @@ public abstract class AbstractApplicationServlet extends HttpServlet { } else if (requestType == RequestType.OTHER) { /* - * TODO Should any URL request really create a new application - * instance if none was found? + * I.e URIs that are not application resources or static (theme) + * files. */ return true; @@ -890,7 +891,7 @@ public abstract class AbstractApplicationServlet extends HttpServlet { return false; } - private void handleServiceSessionExpired(HttpServletRequest request, + void handleServiceSessionExpired(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { if (isOnUnloadRequest(request)) { @@ -1080,7 +1081,7 @@ public abstract class AbstractApplicationServlet extends HttpServlet { } enum RequestType { - FILE_UPLOAD, UIDL, OTHER; + FILE_UPLOAD, UIDL, OTHER, STATIC_FILE, APPLICATION_RESOURCE; } protected RequestType getRequestType(HttpServletRequest request) { @@ -1088,9 +1089,38 @@ public abstract class AbstractApplicationServlet extends HttpServlet { return RequestType.FILE_UPLOAD; } else if (isUIDLRequest(request)) { return RequestType.UIDL; + } else if (isStaticResourceRequest(request)) { + return RequestType.STATIC_FILE; + } else if (isApplicationRequest(request)) { + return RequestType.APPLICATION_RESOURCE; } - return RequestType.OTHER; + + } + + private boolean isApplicationRequest(HttpServletRequest request) { + String path = getRequestPathInfo(request); + if (path != null && path.startsWith("/APP/")) { + return true; + } + return false; + } + + private boolean isStaticResourceRequest(HttpServletRequest request) { + String pathInfo = request.getPathInfo(); + if (pathInfo == null || pathInfo.length() <= 10) { + return false; + } + + if ((request.getContextPath() != null) + && (request.getRequestURI().startsWith("/VAADIN/"))) { + return true; + } else if (request.getRequestURI().startsWith( + request.getContextPath() + "/VAADIN/")) { + return true; + } + + return false; } private boolean isUIDLRequest(HttpServletRequest request) { @@ -1830,6 +1860,13 @@ public abstract class AbstractApplicationServlet extends HttpServlet { application, assumedWindow); } + /** + * Returns the path info; note that this _can_ be different than + * request.getPathInfo() (e.g application runner). + * + * @param request + * @return + */ String getRequestPathInfo(HttpServletRequest request) { return request.getPathInfo(); } diff --git a/src/com/vaadin/terminal/gwt/server/GAEApplicationServlet.java b/src/com/vaadin/terminal/gwt/server/GAEApplicationServlet.java index 13d01cfb49..e24f405ac1 100644 --- a/src/com/vaadin/terminal/gwt/server/GAEApplicationServlet.java +++ b/src/com/vaadin/terminal/gwt/server/GAEApplicationServlet.java @@ -1,6 +1,10 @@ package com.vaadin.terminal.gwt.server; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; import java.util.Date; import javax.servlet.ServletException; @@ -8,64 +12,135 @@ 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.Key; +import com.google.appengine.api.datastore.KeyFactory; 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; -import com.vaadin.ui.Window; +import com.vaadin.service.ApplicationContext; public class GAEApplicationServlet extends ApplicationServlet { private static final long serialVersionUID = 2179597952818898526L; - private static final String MUTEX_BASE = "vaadin.gae.mutex."; + // 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; - private static final int KEEP_MUTEX_MILLISECONDS = 100; + + // Properties used in the datastore + private static final String PROPERTY_EXPIRES = "expires"; + private static final String PROPERTY_DATA = "data"; @Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + 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 = null; + String mutex = MUTEX_BASE + session.getId(); + memcache = MemcacheServiceFactory.getMemcacheService(); try { - RequestType requestType = getRequestType(request); - if (requestType == RequestType.UIDL) { - memcache = MemcacheServiceFactory.getMemcacheService(); - mutex = MUTEX_BASE + request.getSession().getId(); - // try to get lock + // 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) { - // could not obtain lock, tell client to retry - request.setAttribute("noSerialize", new Object()); - response - .setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE); - // Note: currently interpreting Retry-After as ms, not sec - response.setHeader("Retry-After", "" - + RETRY_AFTER_MILLISECONDS); - return; + if (locked) { + break; + } + try { + Thread.sleep(RETRY_AFTER_MILLISECONDS); + } catch (InterruptedException e) { + System.err + .println("Thread.sleep() interrupted while waiting for lock. Trying again."); + e.printStackTrace(System.err); } + } + 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); - if (request.getAttribute("noSerialize") == null) { - // Explicitly touch session so it is re-serialized. - HttpSession session = request.getSession(false); - if (session != null) { - session - .setAttribute("sessionUpdated", new Date() - .getTime()); - } - } + // 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) { - System.err.println("DeadlineExceeded for " - + request.getSession().getId()); + System.err.println("DeadlineExceeded for " + session.getId()); // TODO i18n? criticalNotification( request, @@ -76,21 +151,75 @@ public class GAEApplicationServlet extends ApplicationServlet { } finally { // "Next, please!" if (locked) { - memcache.delete(mutex, KEEP_MUTEX_MILLISECONDS); + memcache.delete(mutex); } - + cleanSession(request); } } - protected boolean handleURI(CommunicationManager applicationManager, - Window window, HttpServletRequest request, - HttpServletResponse response) throws IOException { + 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(WebApplicationContext.class.getName(), + applicationContext); + } catch (IOException e) { + System.err + .println("Could not de-serialize ApplicationContext for " + + session.getId() + + " A new one will be created."); + e.printStackTrace(); + } catch (ClassNotFoundException e) { + System.err + .println("Could not de-serialize ApplicationContext for " + + session.getId() + + " A new one will be created."); + e.printStackTrace(); + } + } + // will create new context if the above did not + return WebApplicationContext.getApplicationContext(session); + + } - if (super.handleURI(applicationManager, window, request, response)) { - request.setAttribute("noSerialize", new Object()); - return true; + /** + * 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(WebApplicationContext.class.getName()); } - return false; } } diff --git a/src/com/vaadin/tests/appengine/GAESyncTest.java b/src/com/vaadin/tests/appengine/GAESyncTest.java index 0f5c85b239..a28da32c39 100644 --- a/src/com/vaadin/tests/appengine/GAESyncTest.java +++ b/src/com/vaadin/tests/appengine/GAESyncTest.java @@ -75,7 +75,7 @@ public class GAESyncTest extends Application { Button b = new Button("Slow", new Button.ClickListener() { public void buttonClick(ClickEvent event) { try { - Thread.sleep((40000)); + Thread.sleep(15000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); -- 2.39.5