Synchronous/Asynchronous Switching with Varnish

When your webapp is serving up content that’s expensive to generate, you may want to serve it up asynchronously- via AJAX calls. This is particularly appealing when content is “below the fold.”

However when that content is cached, you want to serve it up as quickly as possible. If you’ve already calculated the content, you’d like to include it inline in the page, without requiring an AJAX roundtrip. That way, you avoid the latency of an unnecessary round-trip. You also allow the page to be fully rendered (so content doesn’t jump around), etc.

You can optimize for the empty cache, or you can optimize for the full cache, but it seems hard to optimize both experiences.

Redfin faces exactly this conundrum with our listing pages (e.g. http://www.redfin.com/CA/San-Francisco/830-El-Camino-Del-Mar-94121/home/604622.) Calculating the Similar Listings and Similar Sales is expensive and performed in real time. We cut this Gordian Knot through the use of the Varnish caching reverse proxy, along with clever use of ESI (Edge Side Includes.) For an overview of how we use Varnish at Redfin, see our previous post.

We want to say “if there’s a cache miss, then do AJAX, but if there’s a cache hit, then just include the content.” We have to make sure that the AJAX calls will fill the cache, such that subsequent requests will see cache hits, of course!

I’ll outline what the requests/responses look like for us, then I’ll include some pseudocode that supports this.

At the beginning of time, the cache is empty, and the browser requests information on a Listing.

Step Browser Varnish Backend Server
1 Requests http://www.redfin.com/…/home/604622
2 Passes request to server
3 Returns HTML including an ESI like <esi:include src=”/similars?property_id=604622″ />
4 Lookup /similars?property_id=604622 in cache
5 Cache lookup fails
6 Makes request to /similars?property_id=604622
7 Returns HTML for AJAX for Similars (e.g. a <script> block with a reference to http://www.redfin.com/extranet-similars?property_id=604622)
Response includes “no cache” headers
8 Injects the <script> block into the HTML to be returned
Does NOT cache the server response
9 Returns HTML to Browser
10 Displays HTML
11 Executes <script> block
12 Requests http://www.redfin.com/extranet-similars?property_id=604622, including a special header saying “gimme the real content”
13 Passes /extranet-similars?property_id=604622 request to server
14 Returns HTML including an ESI like <esi:include src=”/similars?property_id=604622″ />
15 Lookup /similars?property_id=604622 in cache
16 Cache lookup fails
17 Makes request to /similars?property_id=604622, passing along special “gimme the real content” header
18 Examines request, sees special “gimme the real content” header
19 Calculates correct HTML to display Similar Listings and Similar Sales
20 Returns HTML including “please cache this” headers
21 Injects the Similars block into the HTML to be returned
DOES cache the server response
22 Returns HTML to Browser
23 Client side Javascript injects Similars HTML into page

That’s all great, but we still haven’t used the cache! The cache entry will get used for subsequent requests for the same page, like this:

Step Browser Varnish Backend Server
1 Requests http://www.redfin.com/…/home/604622
2 Passes request to server
3 Returns HTML including an ESI like <esi:include src=”/similars?property_id=604622″ />
4 Lookup /similars?property_id=604622 in cache
5 Cache lookup SUCCEEDS
6 Injects the Similars block into the HTML to be returned
7 Returns HTML to Browser
8 Displays HTML including Similars (no AJAX calls)

There are two things worth noting about this exchange.

First, when the backend server gets a request for /similars?property_id=604622, it has to decide if it should be returning the real HTML, or should be returning Javascript that will retrieve the HTML via AJAX. It makes this decision based on the value of a header passed in by the client. When the client is making an AJAX request, it knows it better NOT get back a response that generates AJAX requests (that’d be a death spiral.) Therefore, when it makes the AJAX request, it includes the special header. In all other cases, the special header is NOT included. When the header is included in a request, the server will generate the real HTML. When the header is not included, Varnish may answer the request from cache, or it may pass through to the backend server. If the request is fulfilled by the Varnish cache, then it’s the real HTML, but if it’s fulfilled by the backend server, it’ll be the AJAXy HTML.

Second, there are two URLs that have to do with similars.

/similars?property_id=604622 is an internal-use-only URL that returns the content (either the proper HTML or the AJAX code.)

/extranet-similars?property_id=604622 is an externally facing URL that only returns an ESI fragment (which will subsequently be filled in by Varnish. This way, the ESI endpoints are never available to the extranet; Varnish can get to them, but extranet clients have no need for them. This lets us be lazy with the ESI URLs. For example, URLs that are exposed to the extranet do extra validation to check if the user is logged in, etc. URLs for internal use only, such as the ESI URLs, can skip that work. This also lets us change the URLs when the property changes, to facilitate cache busting (see the “Cache busting” section in ESI and Caching Trickery in Varnish for more information.

Pseudocode

OK, so we know what we want the interaction to look like. What code will make this happen? Here’s some Javaish pseudocode that illustrates how it might work:


/*
Invoked for requests like http://www.redfin.com/[address]/home/[property id]
*/
public void handlePropertyRequest(Request request, Response response, long propId) {
   Property property = getProperty(propId);
   response.write("<html><head></head><body>" +
      ...
      "<esi:include src='/extranet-similars?property_id=" +
         propId +
         "&last_mod=" +
         property.getLastModified() +
      "'/>" +
      ...
      "</body></html>");
}


/*
Invoked for (extranet) requests like /extranet-similars?property_id=[property id]&last_mod=[date]
*/
public void handleExtranetSimilarsRequest(Request request, Response response, long propId) {
   Property property = getProperty(propertyId);
   response.write("<esi:include src='/extranet-similars?property_id=" +
         propId +
         "&last_mod=" +
         property.getLastModified() +
      "'/>");
}


/*
Invoked for (intranet) requests like /similars?property_id=[property id]&last_mod=[date]
*/
public void handleSimilarsRequest(Request request, Response response, long propId) {
   if (null == request.getHeader("full_html")) {
      //This request does NOT demand that we return the actual HTML.
      // We will return a script block that will fetch the HTML via AJAX.
      response.write("<script>" +
         "dojo.addOnLoad(" +
            "function() {" +
               "dojo.xhrGet({" +
                  "url: 'http://www.redfin.com/extranet-similars?property_id=" + propId + "'," +
                  "load: function(response, ioArgs){" +
                     "dojo.byId('similar_homes').innerHTML = response;" +
                     "return response;" +
                  "}," +
                  "headers: {'full_html': 'true'}," +
                  "handleAs: 'text'" +
               "});" +
            "}" +
         ");" +
         "</script>");
      //Do NOT cache the script
      response.setCacheable(false);
   }
   else {
      //This request wants the actual HTML for similars
      response.write(getSimilarsHTML(propId));
      //The similars HTML is cacheable- that's the whole point!
      response.setCacheable(true);
   }
}

Discussion

  • http://gen-psp.ru Alex KZ

    Why pages of your site downloading too long?

  • http://chistka-kovrov-divanov.ru JPMorgan

    Because they are very large and complex.

  • http://moika-okon-vitrin.ru Moika stekol

    All will be good

  • http://mojkaokon.ru Sov

    Because that very happy http://www.library.ru/2/ yessss

  • http://www.markten-pow.ca/ Homes for sale in Ajax

    Hi,

    guys, In this article in some places, It article is depending on best information about Ajax.