David Chandler's Journal of Java Web and Mobile Development

  • David M. Chandler

    Web app developer since 1994 and Google Cloud Platform Instructor now residing in Colorado. Besides tech, I enjoy landscape photography and share my work at ColoradoPhoto.gallery.

  • Subscribe

  • Enter your email address to subscribe to this blog and receive notifications of new posts by email.

    Join 224 other followers

  • Sleepless Nights…

    March 2010
    S M T W T F S
  • Blog Stats

    • 1,034,589 hits

Archive for March, 2010

AppEngine Cold Starts Considered

Posted by David Chandler on March 26, 2010

I’ve been able to reduce my cold start time on AppEngine from an average of 8.1s to 2.5s, a 69% reduction. If you’re already familiar with the cold start issue, you can skip the next paragraph.

Cold starts seem to be the #1 complaint about AppEngine for Java. The root issue is that many developers coming to AppEngine are expecting it to work like the enterprise Java stacks they work on during their day jobs. AppEngine can indeed run an amazing variety of enterprise frameworks like Spring and JSF (see Will it play in AppEngine), but the reason Google can give it away for free to get started is because they’re not running dedicated servers just for your app. Instead, Google has done what few, if any, Web hosting companies have ever been able to do, which is to provide secure shared hosting for Java. If your app is not getting used at the moment, Google will swap out your JVM and fire it up again when a request comes in, thereby freeing up server memory for active apps. Unfortunately, for lightly used apps, this means AppEngine may have to spin up your JVM for every new user session. This includes initializing the servlet context and all frameworks your app may be using. Consequently, cold start times between 8-12s are not uncommon, and some larger stacks can’t even start within in the 30s request deadline.

Frankly, 10s isn’t a bad time to start up a Java stack. Most enterprise apps I’ve worked on take a minute or more. AppEngine doesn’t take that long because lots of AppEngine services are always running: Datastore, Memcache, etc. The problem is that due to the shared nature of the AppEngine platform, you have to pay this startup penalty very often. Lots of folks have asked Google to create a pay-for-warm-JVM option, which has recently been added to the AppEngine for Java roadmap. A nice way to do this might be set a minimum fee for billing-enabled apps that would guarantee you a JVM, but which you could credit toward actual resource usage.

For now, however, you can reduce your cold start time by rethinking (and refactoring) your app to work with AppEngine rather than trying to force your enterprise stack on the platform.

Let’s start with dependency injection. I love DI frameworks like Spring and Guice. But they’re not designed for shared hosting. They deliberately pre-load as much as possible at startup, which is the right thing to do on a dedicated server when startups are infrequent. But on AppEngine, it will cost you. Guice on AppEngine is configured via a servlet context listener. Unfortunately, this means that every servlet, including cron jobs and task invocations, trigger Guice initialization during a cold start, even though those servlets don’t need any Guice-provided objects. Worse, Guice eagerly loads all singletons in production mode, so all my gwt-dispatch ActionHandlers were getting loaded with every hourly cron job (which doesn’t even need the ActionHandlers). The solution was to replace Guice with gwt-dispatch’s basic LazyActionHandlerRegistry as described on the wiki. That saved me 5+ seconds.

Next, consider a fast-startup persistence framework like Objectify-appengine. JPA and JDO incur significant overhead to create a new PersistenceManager (by some reports, 2-3s). “So,” you say, “I’ll use a DI framework to create only one instance of the PM at startup,” and now you’re back to the previous paragraph. A better alternative for AppEngine is to use a lightweight persistence framework designed specifically for AppEngine. ObjectifyService.begin() takes only miliseconds, and IMHO is easier to use than JDO or JPA, although I appreciate that Google makes those APIs available for portability.

Bottom line: by eliminating DI frameworks and using Objectify for persistence, I’m seeing cold starts in 2.5s average. I am more than happy to pay this small penalty for the privilege of running my code for free on the world’s most scalable Java infrastructure. This approach also conserves community resources vs. running a useless cron job just to keep your app warm, which makes cold starts happen all that more often for the rest of us.

Of course, once my app traffic takes off or Google come out with a pay-for-JVM option, all this goes away and you can use dependency injection, cron jobs, etc. with abandon. But for now, the name of the game is, how small can you make your app? Personally, I love the challenge. I much prefer lightweight, plain old Java to layer upon layer of frameworks, anyway.

Posted in AppEngine, GIN / Guice | 10 Comments »

Adding info to GWT-RPC dispatch URL for logs

Posted by David Chandler on March 19, 2010

I was digging around GWT a little this week and found a neat way to append information to server requests sent by gwt-dispatch. By default, dispatch requests show up in the logs as just “/your_app_path/dispatch”. But it turns out you can add a few lines to your DispatchAsync class to append the action name, so you see “/your_app_path/dispatch/SomeAction” in the server logs.

This is really cool on AppEngine because the Dashboard aggregates the number of requests and total CPU for each URL. With the basic dispatcher, all requests to “/your_app_path/dispatch” get rolled up together, but with this enhancement, you get dashboard roll-ups for each Action.

All you need is a couple extra lines in StandardDispatchAsync. In addition, you must append a wildcard to your dispatch servlet mapping in web.xml, “/your_app_path/dispatch/*” (thanks to Ben Binford for pointing this out). The changes from gwt-dispatch are in lines 27 and 35-38.

package net.customware.gwt.dispatch.client.standard;

import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.rpc.ServiceDefTarget;

import net.customware.gwt.dispatch.client.AbstractDispatchAsync;
import net.customware.gwt.dispatch.client.DispatchAsync;
import net.customware.gwt.dispatch.client.ExceptionHandler;
import net.customware.gwt.dispatch.server.Dispatch;
import net.customware.gwt.dispatch.shared.Action;
import net.customware.gwt.dispatch.shared.Result;

 * This class is based on the default implementation of {@link DispatchAsync}, which is
 * essentially the client-side access to the {@link Dispatch} class on the
 * server-side.
 * This version appends the name of the Action class to the URL as extra path info.
 * @author David Peterson
 * @author David Chandler
public class StandardDispatchAsync extends AbstractDispatchAsync {

    private static final StandardDispatchServiceAsync realService = GWT.create( StandardDispatchService.class );
    private static final String baseUrl = ((ServiceDefTarget)realService).getServiceEntryPoint() + "/";

    public StandardDispatchAsync( ExceptionHandler exceptionHandler ) {
        super( exceptionHandler );

    public <A extends Action<R>, R extends Result> void execute( final A action, final AsyncCallback<R> callback ) {
        // Append action class name as extra path info
        String className = action.getClass().getName();
        int namePos = className.lastIndexOf(".") + 1;
        className = className.substring(namePos);
        ((ServiceDefTarget)realService).setServiceEntryPoint(baseUrl + className);

        realService.execute( action, new AsyncCallback<Result>() {
            public void onFailure( Throwable caught ) {
                StandardDispatchAsync.this.onFailure( action, caught, callback );

            public void onSuccess( Result result ) {
                StandardDispatchAsync.this.onSuccess( action, (R) result, callback );
        } );

There are other possible uses of the extra path info. See this forum thread for discussion of security uses and the corresponding warnings.


Posted in AppEngine, Google Web Toolkit | Leave a Comment »

Coding getLogger() quickly with an Eclipse template

Posted by David Chandler on March 12, 2010

The AppEngine admin console log viewer is so powerful that it makes me want to log anything and everything. One obstacle to this (however minor) is that it’s a pain to write this for every class in which I want to log:

private static final Logger LOG = Logger.getLogger(MyLongClassName.class.getName());

Eclipse code templates to the rescue! Open Window | Preferences | Java | Editor | Templates and create a new template. Name it “log”, set the context to “Java type members”, and enter this pattern:

private static final Logger LOG = Logger.getLogger(${enclosing_type}.class.getName());

Click OK to save it. Now open any class in the editor, and type “log” followed by Ctrl-Space. Press Enter. Presto!

I feel a template for AsyncCallbacks coming on…

Posted in Eclipse | 3 Comments »

Server-side browser detection in a servlet filter

Posted by David Chandler on March 11, 2010

Second things first: if you need a polite, pretty, and usable way to help your users upgrade their browser, point them to WhatBrowser.org, created just for that purpose.

But first you have to find out what browser they’re currently running. I’ve implemented this as a servlet filter on my signup and login pages so as to inform users as early as possible if their browser is inadequate. The basic idea is to find some part of the User-Agent header that uniquely identifies each browser. “WebKit” and “Mozilla” are out because these appear in so many browsers. Fortunately, most browsers have their common name in the User-Agent string somewhere. By default, the filter allows Chrome, Firefox, Safari, Opera, and IE versions 6-8. You can configure the filter with your own list if desired.

package com.turbomanage.gwt.server.servlet;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.google.inject.Singleton;

public class BrowserFilter implements Filter
	// Be default, support all GWT-capable browsers
	// Assume any version recent enough except IE
	private static final String[] DEFAULT_BROWSERS =
	{ "Chrome", "Firefox", "Safari", "Opera", "MSIE 8", "MSIE 7", "MSIE 6" };

	// Filter param keys
	public static final String KEY_BROWSER_IDS = "browserIds";
	public static final String KEY_BAD_BROWSER_URL = "badBrowserUrl";

	// Configured params
	private String[] browserIds;
	private String badBrowserUrl;

	public void init(FilterConfig cfg) throws ServletException
		String ids = cfg.getInitParameter(KEY_BROWSER_IDS);
		this.browserIds = (ids != null)?ids.split(","):DEFAULT_BROWSERS;

		badBrowserUrl = cfg.getInitParameter(KEY_BAD_BROWSER_URL);
		if (badBrowserUrl == null)
			throw new IllegalArgumentException("BrowserFilter requires param badBrowserUrl");

	public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
		throws IOException, ServletException
		String userAgent = ((HttpServletRequest) req).getHeader("User-Agent");
		for (String browser_id : browserIds)
			if (userAgent.contains(browser_id))
				chain.doFilter(req, resp);
		// Unsupported browser
		((HttpServletResponse) resp).sendRedirect(this.badBrowserUrl);

	public void destroy()
		this.browserIds = null;

I’m using Guice, so this is what my configuration looks like. Of course, you can also use standard web.xml configuration instead.

package com.roa.server.guice;

import java.util.HashMap;

import com.google.inject.Singleton;
import com.google.inject.servlet.ServletModule;
import com.turbomanage.gwt.server.servlet.BrowserFilter;

public class DispatchServletModule extends ServletModule
	public void configureServlets()
		HashMap<String, String> filterCfg = new HashMap<String,String>();
		filterCfg.put(BrowserFilter.KEY_BROWSER_IDS, "Chrome,Firefox,Safari,Opera,MSIE 8");
		filterCfg.put(BrowserFilter.KEY_BAD_BROWSER_URL, "/s/bb.html");
		filter("/roa/signup/signup.jsp", "/roa/app/login", "/roa/app/index.html").through(
			BrowserFilter.class, filterCfg);

You initialize the filter with one or two parameters. The badBrowserUrl param is required. The filter will redirect any unsupported browser to this location. You can optionally set your own comma-separated list of browser identification strings in the browserIds param.

For further research on browser server-side identification, check out http://www.zytrax.com/tech/web/browser_ids.htm.

Posted in GIN / Guice, Google Web Toolkit | 3 Comments »

Which way to MVP? and other notes from DevNexus

Posted by David Chandler on March 10, 2010

I had the privilege of speaking at DevNexus this week (conf was a great success, with 300+ developers  from the Atlanta area), and the challenge of speaking on AppEngine and GWT MVP immediately after Google engineers spoke on the same topics. It was great to meet AppEngine for Java co-creator Toby Reyelts and GWT engineer Chris Ramsdale, both of whom are frequently seen on the forums.

Chris and I presented slightly different ways of doing MVP. Ray Ryan’s Google I/O 2009 presentation that started the MVP craze showed the approach taken by gwt-presenter (which I showed), where each view implements an interface defined by the corresponding presenter. Other Google teams have subsequently modified this approach and define interfaces for both presenter and view. This simplifies DOM event handling. In Ray Ryan’s original model, the presenter would add a ClickHandler to a Widget implementing HasClickHandlers. In the double-interface model, the view could instead add the ClickHandler and call a method on the presenter interface. I have definitely run into situations where the latter would be handy, but I don’t have a strong opinion either way, and Chris was very careful not to be dogmatic, either. There are lots of good ideas out there. Someone at my GWT talk asked if I had seen mvp4g (I hadn’t, thanks!), and claimed it has runAsync support for presenters, which sounds promising.

At the end of my talk on gwt-presenter and gwt-dispatch, I presented a few GWT development tools (Firefox Web Developer Toolbar and Firebug) and made the claim that they represent the best there is for WYSIWYG GWT development.

OOPS! Embarrassingly, I hadn’t tried Chrome Developer Tools yet! Google has raised the bar again.

Google raises the bar again

Speed Tracer also looks very promising for analyzing GWT, in particular.

Having been “heads-down” on AppEngine and GWT for the last 6 months, it was good to take a breather and catch up on new developments.

Posted in AppEngine, Google Web Toolkit | 2 Comments »

A few words on AppEngine logging (production)

Posted by David Chandler on March 5, 2010

The AppEngine production log viewer is the envy of most development and sysadmin teams I’ve ever worked with. It offers a a near real-time view into your application’s doings in a powerful Web-based interface. In addition to filtering on the standard JDK log levels, you can search a particular time period, filter any regex, and use labels to filter on

  • day,
  • month,
  • year,
  • hour,
  • minute,
  • second,
  • tzone,
  • remotehost,
  • identd_user,
  • user,
  • status,
  • bytes,
  • referrer,
  • useragent,
  • method,
  • path,
  • querystring,
  • protocol

The request log shows every request, even images and CSS, so you can see why you need to turn on expiration headers for static files 🙂 RPC calls, cron jobs, task servlets–they’re all there. Furthermore, if your app is enabled for Google account authentication, every request is tagged with the Google user id. This is a poor man’s “fish tag” and is extremely helpful for real-time customer support with your mother-in-law (“Mom! You’re using IE 5!”).

If you’re using gwt-dispatch, you’ll notice that all requests to the dispatcher are logged alike (which also causes the Dashboard CPU meter to aggregate all requests to your dispatch servlet). With this in mind, I’m thinking of having the dispatch servlet log the Action class being invoked so I can put together something akin to a click trail. In addition, I wonder if there’s a way for the GWT dispatcher to append the action name as extra path info or query string? This way, the Dashboard might be able to break out which Actions are consuming the most CPU.

The only unpleasantness I’ve found is that AppEngine clutters up the logs with a bunch of harmless warnings about FinalizableReferenceQueue. You can easily get rid of them, however, by putting these lines to your WEB-INF/logging.properties:

# Hide harmless

Actually, there is one more unpleasantness: the much-discussed cold start problem. If your app is not yet world-famous, AppEngine will cycle it out after only a few minutes of non-use. This issue has been beat to death on the forums, but suffice it to say it’s easy to spot: just look for the requests with CPU time in red that took 8 seconds after a period of inactivity. Lots of folks have said they’d be willing to stay “hot,” which seems like a good idea. For my part, I like the platform enough that I’m willing to bet Google will take this seriously.

Posted in AppEngine | 4 Comments »

A recipe for unit testing AppEngine task queues

Posted by David Chandler on March 3, 2010

One of the difficulties of testing AppEngine code has been the problem of running unit tests for code written against the Task Queue API. AppEngine provided no easy way to programatically control the task queue engine. In addition, tasks are servlets, so it’s not particularly easy to invoke them with the correct task payload in a unit test.

Fortunately, AppEngine 1.3.1 provides test helpers that greatly simplify writing unit tests against the Datastore and Task Queue APIs (as well as Memcache, Blobstore, and others). By default, the Datastore is configured with the NO_STORAGE property set to true (the default) so that each test method starts with a blank datastore. Also by default, the task queuing auto-run capability is disabled, which gives you the opportunity to programatically run a task once you have verified that it’s been enqueued properly.

The following BaseTest class initializes the Datastore and TaskQueue services. Once the AppEngine team fixes the bug that requires the LocalServerEnvironment override, it will be very simple, indeed. You can ignore the Guice injector stuff if you’re not using Guice.


package com.roa.test.helper;

import java.io.File;

import junit.framework.TestCase;

import com.google.appengine.tools.development.LocalServerEnvironment;
import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
import com.google.appengine.tools.development.testing.LocalTaskQueueTestConfig;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.roa.server.guice.ServerModule;
import com.turbomanage.gwt.server.guice.DispatchTestModule;

 * A simple test base class that can be extended to build unit tests that
 * properly construct and tear down AppEngine test environments.
public abstract class BaseTest extends TestCase

	private static Injector inj;
	protected LocalServiceTestHelper helper;
	private LocalDatastoreServiceTestConfig datastoreConfig;
	private LocalTaskQueueTestConfig taskQueueConfig;

	 * Sets up the AppEngine environment and initializes Guice.
	protected void setUp() throws Exception
		datastoreConfig = new LocalDatastoreServiceTestConfig();
		taskQueueConfig = new LocalTaskQueueTestConfig();
		helper = new LocalServiceTestHelper(datastoreConfig, taskQueueConfig)
			// Temp workaround until 1.3.2 to help task queue find war/WEB-INF/queue.xml
			protected LocalServerEnvironment newLocalServerEnvironment()
				final LocalServerEnvironment lse = super.newLocalServerEnvironment();
				return new LocalServerEnvironment()
					public File getAppDir()
						return new File("war");

					public String getAddress()
						return lse.getAddress();

					public int getPort()
						return lse.getPort();

					public void waitForServerToStart() throws InterruptedException


		inj = Guice.createInjector(new ServerModule(), new DispatchTestModule());

	protected void runTest() throws Throwable

	 * Deconstructs the AppEngine environment.
	protected void tearDown() throws Exception

	 * Provide Guice injector to tests
	protected static Injector getInj()
		return inj;


Code under test will now have access to the Datastore and the TaskQueue API just as in dev and production environments.

The new task queue helper (LocalTaskQueueTestConfig) provides methods to inspect the task queue and verify task creation. When you call runTask(), the task queue test service will attempt to invoke the servlet associated with your task; however, unless you can figure out a way to run your unit test in a servlet container, your task servlet won’t actually be reachable by the runTask() call (at least, I’m assuming this is why I’m seeing “connection refused” errors).

Fortunately, we can easily simulate what the AppEngine task queue does by invoking the task servlet using ServletUnit. The test case below creates a simple test case using Vince Bonfanti’s Deferred task servlet, verifies that it has been enqueued properly, and then invokes it using ServletUnit.

package com.roa.test;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.Map;

import javax.servlet.ServletException;

import org.xml.sax.SAXException;

import com.google.appengine.api.labs.taskqueue.dev.LocalTaskQueue;
import com.google.appengine.api.labs.taskqueue.dev.QueueStateInfo;
import com.google.appengine.api.labs.taskqueue.dev.QueueStateInfo.TaskStateInfo;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.appengine.tools.development.testing.LocalTaskQueueTestConfig;
import com.meterware.httpunit.PostMethodWebRequest;
import com.meterware.servletunit.ServletRunner;
import com.meterware.servletunit.ServletUnitClient;
import com.newatlanta.appengine.taskqueue.Deferred;
import com.newatlanta.appengine.taskqueue.Deferred.Deferrable;
import com.roa.test.helper.BaseTest;

public class SimpleTaskTest extends BaseTest implements Serializable

	protected void setUp() throws Exception
		// Do additional setup here

	public class HelloUserTask implements Deferrable
		private User user;

		public HelloUserTask(User u)
			this.user = u;

		public void doTask() throws ServletException, IOException
			System.out.println("Hello, " + user.getNickname());

	public void testTaskInvocation() throws IOException, SAXException
		User testUser = UserServiceFactory.getUserService().getCurrentUser();
		HelloUserTask task = new HelloUserTask(testUser);
		// Queue the task

		// Verify that one task has been enqueued
		LocalTaskQueue taskQueue = LocalTaskQueueTestConfig.getLocalTaskQueue();
		Map<String, QueueStateInfo> allQueues = taskQueue.getQueueStateInfo();
		QueueStateInfo deferredQueue = allQueues.get("deferred");
		assertEquals(1, deferredQueue.getCountTasks());
		TaskStateInfo taskInfo = deferredQueue.getTaskInfo().get(0);
		String queuedTask = taskInfo.getBody();
		String taskName = taskInfo.getTaskName();

		// Run task will fail because no servlet container running
		// taskQueue.runTask("deferred", taskName);

		// Run using ServletUnit instead
		ServletRunner sr = new ServletRunner();
		sr.registerServlet("/_ah/queue/deferred", "com.newatlanta.appengine.taskqueue.Deferred");
		ServletUnitClient client = sr.newClient();
		client.sendRequest(new PostMethodWebRequest("http:/_ah/queue/deferred",
			new ByteArrayInputStream(queuedTask.getBytes()), null));

		// Flush task queue (removes all tasks)


When you run this, you’ll see “Hello, test@example.com” written to the console by the task.

Using the AppEngine test helpers and ServletUnit, we now have an easy way to verify that tasks have been queued as expected AND a way to invoke the task servlets in the same test case.

Happy testing!

Posted in AppEngine | 13 Comments »

Tips for using CachingDispatchAsync with gwt-dispatch

Posted by David Chandler on March 2, 2010

In Chris Lowe’s excellent tutorial on MVP with gwt-presenter and gwt-dispatch, he provides a CachingDispatchAsync which nicely demonstrates the power of the Command pattern. When you call executeWithCache(), the dispatcher will first check to see if the Result associated with the Action has previously been cached, and if so, will return it and save a trip to the server.

When I first played with CachingDispatchAsync, I thought it wasn’t working because I never saw anything returned from cache, but it turns out I’d overlooked one of the fundamental rules of using Collections (see Josh Bloch’s Effective Java, Item 9: When you override equals(), override hashCode()). CachingDispatchAsync uses a HashMap to implement the cache, so a cache “hit” succeeds only when the Actions being compared are the identical instance (==) or satisfy equals() and hashCode().

To succeed with CachingDispatchAsync, you can use something like this:

SomeAction<SomeResult> someAction = new SomeAction();
// pass Action instance, not "new SomeAction()"
cachingDispatch.executeWithCache(someAction, ...)
// Second time using SAME action instance as before instead of new SomeAction()
// so cache will be hit
cachingDispatch.executeWithCache(someAction, )

Most of the time, however, your dispatch calls will occur in separate classes and won’t have access to the same Action instance. In that case, you need to override equals() and hashCode(). This example Action has no fields; therefore hashCode() and equals() return the same values for any instance of the class.

package com.roa.app.shared.rpc;

import net.customware.gwt.dispatch.shared.Action;

public class FindUserListSubsAction implements Action<FindUserListSubsResult>

	public FindUserListSubsAction()

	public boolean equals(Object obj)
		return this.getClass().equals(obj.getClass());

	public int hashCode()
		return this.getClass().hashCode();


When used properly, CachingDispatchAsync greatly simplifies initialization. If several presenters need the same data, each can simply call for it and let the dispatcher figure out whether a server trip is needed. The alternatives are not pretty: timing problems (what if the user navigates directly here first…?), duplicate requests (easiest to implement, but not optimized), or roll-your-own caching in a service layer. If you choose the latter, I recommend you pass an AsyncCallback to your service calls in case the service needs to make an async call for data. I had been firing events from services instead, and this resulted in a lot of event classes and more event listeners than needed being notified, even circular events in some cases, which are more fodder for the book I will never write but certainly could have: 101 Ways to Write an Infinite Loop). Fortunately, CachingDispatchAsync simply and elegantly solves most of these problems.

As suggested by the Google I/O presentation that started the whole GWT MVP craze, I’ve further beefed up CachingDispatchAsync to do batching and queueing. Batching combines multiple requests for the same Action. This occurs when the first request has not yet returned and therefore has not yet cached the result. It happens a lot at startup. Queuing allows you to specify the order in which Actions are processed and ensures that all actions have been completed before any callbacks are called. This is very useful when doing client-side joins requiring multiple service calls, but that’s for another night… The combination of these three (queuing, batching, and caching) has yielded a robustness to my app that was previously elusive.

Posted in Google Web Toolkit | 3 Comments »

%d bloggers like this: