Brian TakitaBrian Takita
Cacheable Flash
edit Posted by Brian Takita on Wednesday August 08, 2007 at 12:50AM

Page caching is an easy way to get massive performance and scalability increases with little up front effort. Of course, when page caching, a number of design changes are necessary. For example, server side session data cannot be used to render data on cached pages.

Rails provides the flash hash to easily render alert messages and errors. In the controller you can write to the flash hash:

flash[:error] = "You cannot go there"
# or
flash[:notice] = "Welcome to eternity"

and render the flash hash in the view. Typically the rendering happens on a layout:

<div id="error_div_id" class="flash flash_error"><%= flash[:error] %></div>
<div id="notice_div_id" class="flash flash_notice"><%= flash[:notice] %></div>

There are some strange quirks with using flash such as needing to use FlashHash#now when rendering the response without redirecting.

Everything works great until you need to page cache the landing page.

For example, lets say you page cache your home page. After logging in, you are redirected to the home page with the flash notice "Logged in successfully". When page caching, this solution does not work because the request is responded to by the Web Server (i.e. Apache) and does not reach the Rails App server (i.e. Mongrel).

This means the view does not get a chance to render the flash error and notice.

There are a couple of solutions to this problem.

  • Do an AJAX request back to the server to render the flash error and notice (i.e. using RJS)
  • Send the flash error and notice from the server with cookies and render it during page load

Introducing Cacheable Flash

To solve the problem using cookies on Peer to Patent, we wrote the Cacheable Flash plugin. The plugin allows you to set the flash hash as normal on the controller. It handles converting the flash hash into cookies. All you need to do is include the CacheableFlash module into your controller.

class ApplicationController < ActionController::Base
  include CacheableFlash
  # ...
end

On the view side, you will need to add some javascript.

<div id="error_div_id" class="flash flash_error"></div>
<div id="notice_div_id" class="flash flash_notice"></div>
<script type="text/javascript">
  Flash.transferFromCookies();
  Flash.writeDataTo('error', $('error_div_id'));
  Flash.writeDataTo('notice', $('notice_div_id'));
</script>

The Flash.transferFromCookies method:

  • Grabs the flash data from the cookies and saves it to Flash.data
  • Deserializes the flash hash from JSON to Javascript
  • Erases the flash data from the cookies

The Flash.writeDataTo method:

  • Writes data from the passed in key in Flash.data to the passed in element or element id

Here is how the life cycle of the login works:

  1. The user enters the correct login information
  2. Rails handles the web request. In the Login controller, flash[:notice] is written to

      if current_user
        flash[:notice] = "Welcome to Eternity"
      end
    
  3. An after filter serializes contents of the Flash Hash as JSON into cookies

  4. The after filter clears the flash hash
  5. The cached page is rendered
  6. The client side receives and clears the flash cookie data
  7. The client side javascript renders the flash messages

There is also a side benefit -- you don't have to use FlashHash#now because storing the flash in cookies to be rendered and erased by the client makes this unnecessary.

The Cacheable Flash plugin is on the Pivotal RB project page (http://rubyforge.org/projects/pivotalrb). You can install it by running:

ruby script/plugin install svn://rubyforge.org/var/svn/pivotalrb/cacheable_flash/trunk

It will copy flash.js, cookie.js, and json.js if you do not already have these files.

Happy Page Caching!!

Thanks to Josh Susser for pairing with me on this.

Comments

  1. pt pt on August 08, 2007 at 04:50AM

    so sweet.

  2. pt pt on August 08, 2007 at 04:51AM

    so sweet.

  3. szeryf szeryf on August 08, 2007 at 08:48AM

    bookmark added :)

  4. mikeymckay@gmail.com mikeymckay@gmail.com on August 08, 2007 at 05:17PM

    So dumb question - but how do I do the caching part? I have some reports that take forever to run. I want to cache them overnight. At the moment I am doing it via a sloppy hack. But this post implies that rails has a builtin way?

  5. choonkeat choonkeat on August 08, 2007 at 05:36PM

    thanks for getting to it! this community is great :-)

  6. Ismael Ismael on August 08, 2007 at 06:13PM

    Is't it possible to overload flash[:something].to_s so it writes the needed Javascript for you? That way you would render the flash as usual.

  7. Brian Takita Brian Takita on August 08, 2007 at 08:55PM

    Ismael, that is an interesting idea.

    The after filter would need to be changed to generate the javascript. The client side can then eval the contents of the cookie.

    We can remove the 2 Flash.writeDataTo calls by using generating the JS using the key/value pairs in the Hash.

    Nick recommended we use convention for the element ids. For example, the error and notice elements would have the ids "flasherror" and "flashnotice".

    I'd like to have the after filter generate the JS by default, and still have a hook to easily override this behavior.

  8. Brian Takita Brian Takita on August 08, 2007 at 09:08PM

    Mikey, you can use Rails standard page caching and have a cron expire the page, by deleting the page, every night.

    You can also use a cron strategy where your cron does a HTTP request and writes the output to the cached file.

  9. ml ml on August 09, 2007 at 05:22AM

    Hm, how does fallback for non-javascript browsers work?

  10. John John on August 09, 2007 at 02:08PM

    Have you guys thought about modifying this so that you can replace any portion of the page, not just the flash, with a dynamic bit and still cache the whole page? I'm thinking of your login example, showing the logged in users name on the page somewhere as well as the flash.

  11. Brian Takita Brian Takita on August 09, 2007 at 05:16PM

    ml, there is no fallback built into the plugin.

    You could make your own fallback by extracting the JS rendering into a helper method. There can be a state that will either render using javascript or render directly on the server side.

    If you have a non-js browser, it would be difficult (I'm not saying impossible) to render dynamic content using page caching.

  12. Brian Takita Brian Takita on August 09, 2007 at 07:57PM

    John, on Peer to Patent and other projects at Pivotal, we do that by having a javascript conditional in the page.

    For example, there is the header that shows "Hello, John" when you are logged in and the Login links when you are not logged in. We need to render the html for both scenarios to page cache this.

    We a javascript conditional that looks into the cookies to see if you are currently logged in, to decide which html to show.

    This can definitely be the topic of another blog post.

    You can see more about Peer to Patent's caching strategy at Rails, Slashdotted: no problem.

  13. Don Parish Don Parish on August 10, 2007 at 09:49PM

    On Rails 1.2.3, I had to include the json gem in the init.rb of the plugin for it to work for me.

  14. Brian Takita Brian Takita on August 11, 2007 at 08:12PM

    Don, Thank you for the bug report. I fixed it.

  15. thomas thomas on August 17, 2007 at 06:30PM

    I have used similar strategies to page cache pages and they have worked well. On places like the homepage I usually store the user info in a cookie and then construct the html needed on page load

  16. Chris Saylor Chris Saylor on September 23, 2007 at 02:51PM

    Great plugin! I styled my flash divs so I had to hack it a bit. Set a style display: none on the divs. Then inside flash.js move the line

    element.innerHTML = unescape(content);

    inside the if(Flash.data[name]) statement.

    Now add after that line, still inside the if statment, the follow effect

    element.toggle('appear');

    Now my styled flashes only show up when there is content.

  17. weepy weepy on September 27, 2007 at 10:08PM

    Actually I've just written a plugin (called CacheBack) that has similar functionality, but is more generic - so you can cache not just the flash, but other aspects e.g. login details in the nav or show non-sensitive conexts to each logged in user.

    If anyones interested - i can make it public.

    *...(

  18. jasiek jasiek on October 01, 2007 at 01:28PM

    How do I test this thing? It looks like after I do a get :index or whatever, the flash dictionary is emptied.

  19. Doug Doug on October 01, 2007 at 10:27PM

    @weepy - I'd definitely be interested...let me know if you open it up, dougcole at gmail

  20. Brian Takita Brian Takita on October 04, 2007 at 06:25AM

    weepy - Please post the url for your plugin. I couldn't find it.

  21. Brian Takita Brian Takita on October 05, 2007 at 08:33PM

    jasiek - I will add test helpers to the plugin soon. In the mean time, add this to your test or spec helper.

    <code>  def flash_cookie
        return {} unless cookies['flash']
        JSON.parse(cookies['flash'].first)
      end</code>
  22. Tom Tom on October 26, 2007 at 05:28PM

    I'm using the cacheable flash, and loving it, except for one thing:

    Firefox 2.0.0.8 (maybe earlier versions too) and IE 6 seem reluctant to erase the cookie on the client side, so when I refresh a page, or navigate around, my flash information gets displayed for a few pages rather than just one.

    I've tried changing the code that erases the cookie to modify the behavior, but with no success

    Is anyone else seeing this?

  23. Tom Tom on October 26, 2007 at 05:28PM

    I'm using the cacheable flash, and loving it, except for one thing:

    Firefox 2.0.0.8 (maybe earlier versions too) and IE 6 seem reluctant to erase the cookie on the client side, so when I refresh a page, or navigate around, my flash information gets displayed for a few pages rather than just one.

    I've tried changing the code that erases the cookie to modify the behavior, but with no success

    Is anyone else seeing this?

  24. Henrik N Henrik N on December 03, 2007 at 10:55PM

    I ran into an issue on edge Rails where one of my before filters set a flash and redirected: this would prevent the Cacheable Flash after_filter from running at all.

    My fix was simply to make Cacheable Flash use an around_filter instead (doing nothing before the yield) since these aren't interrupted.

  25. Brian Takita Brian Takita on December 06, 2007 at 09:58PM

    Henrik, thanks for the report. I'll add the fix.

  26. Brian Takita Brian Takita on December 06, 2007 at 10:24PM

    Tom, are you still having the issue? Are you doing custom things with your Rails cookies, such as changing the path?

  27. msq msq on December 10, 2007 at 06:44PM

    Thanks! Very useful plugin.

  28. Jonathan C Jonathan C on December 14, 2007 at 01:00AM

    Works like a charm, thanks so much!

  29. Bala Paranj Bala Paranj on May 21, 2008 at 01:17AM

    Brian,

    In my application.html.erb layout I have:

    flash.each do |key,value|
        div id="flash" class="flash_key
            span class="message"  value  span
        div
         end
    

    Should I use the flash as the value for the div id? Will it work?

  30. Brian Takita Brian Takita on June 20, 2008 at 05:57PM

    @Bala - Do you mean using the keys in your flash hash as the div id? There's no reason why it wouldn't work. The thing you may need to be concerned about is defining a flash key of an already existing id on your page.

  31. Michael Erb Michael Erb on June 23, 2008 at 03:38AM

    I added:

    ActionView::Helpers::AssetTagHelper.register_javascript_include_default('cookie','flash','json')

    to cacheable_flash.rb at line 2 so I didn't have to change my layouts.

    Yes, I'm that lazy :o)

  32. Brian Takita Brian Takita on June 25, 2008 at 08:02AM

    @Michael - Thank you. I added your changes.

    Also, I added cacheable-flash to github.

    http://github.com/pivotal/cacheable-flash

  33. Lee Fyock Lee Fyock on January 14, 2009 at 08:02PM

    We're seeing the same issue (I believe) as Tom with IE6 -- the flash hangs around for a few page loads after it should have gone away.

    Also, it's fairly easy to get the flash to concatenate messages on successive page loads, rather than clearing the flash between page loads. Just post form data a couple of times before the entire page has time to load.

    So, on our login page, if you just hit return without putting in a name and password, you see "Invalid user/password combination". Hit return again, and the flash now says "Invalid user/password combination\u003Cbr/\u003EInvalid user/password combination". Keep hitting return, and they keep concatenating. If you allow the page to completely load, then you're OK.

    This is reproducible in FF3 for Mac, Safari for Mac, and IE7, at least.

    Does anyone have any ideas on how to prevent this?

    Does anyone have any ideas on the IE6 issue?

    Thanks! Lee

Add a Comment (MarkDown available)