If you explicitly expire cached fragments, opt out of cache digests

We upgraded a Rails 3 application to use Rails 4, and found that certain pages weren’t being updated as we expected. The problem was that cached fragments were no longer being expired properly.

The problem

Briefly: The application was explicitly expiring cached fragments by name. After the upgrade to Rails 4, this code didn’t have the intended effect because the templates were creating different fragment names; the application was no longer expiring the fragment names that the templates were actually using.

Let’s look at this in more detail. In Rails 3, you could have a template with this code:

<% cache 'hello' do %>
...
<% end %>

This would cache the fragment in the block under a name derived from your argument. In this example, we supplied the argument “hello” and the name generated would be “views/hello”.

Elsewhere in your application, you could explicitly expire that fragment, so that the next time the fragment was referenced, it would be regenerated. You’d do that by sending this message to a controller object:

expire_fragment('hello')

This would generate the full name, “views/hello”, and expire the cached fragment with that name.

Say we start using Rails 4. Now the caching code in the template appends a new piece of information – a cache digest – to the fragment name it generates. The cache digest will depend on the current template and its dependencies. In our example, the fragment name might be something like “views/hello/e3225f7c7bbadd41c6ea556b7b7a03b1”.

The code that expires the fragment, however, is outside the context of this template, so it does not know the cache digest. As with Rails 3, it will try to expire the fragment named “views/hello” – but that’s not what our fragment is called anymore. The fragment, then, is no longer being expired.

Solutions

How do we fix this?

Well, ideally, the application wouldn’t be expiring fragments explicitly. Instead of a string literal, the template would pass a relevant object as the argument to cache, and that object would generate its own cache key. The code for generating the key would be carefully designed so that the key would only change if the object changed in such a way that it might need to be presented differently.

But you go to war with the code you have, not the code you wish you could have. If the application will continue to expire fragments explicitly, then one simple solution is to stop appending the cache digest. We found every fragment we were trying to explicitly expire and passed the option :skip_digest => true to cache. So the template code looked like this:

<% cache 'hello', :skip_digest => true do %>
...
<% end %>

This solved our problem – well enough for the time being, at least.

By the way: If you ran into this problem during a Rails upgrade, the addition of cache digests created some inconvenience for you. Don’t get annoyed, though. This is, in fact, a very cool feature of Rails 4: cache digests allow for Russian doll caching.