Old School Cache Invalidation in a world of Rails 5.2 Recyclable Cache Keys

One of the hallmark features of Rails 5.2 was the introduction of recyclable cache keys.

Once the new cache API was integrated into Basecamp, DHH had this to say about it:

We went from only being able to keep 18 hours of caching to, I believe, 3 weeks. It was the single biggest performance boost that Basecamp 3 has ever seen.

Put simply, the previous cache key generation (unless you over-wrote it) used the updated_at timestamp to differentiate the specific version fo the object. So for example [class]/[id]-[timestamp], like users/5-20110218104500 or thing/15-20110218104500, which is what Active Record in Rails used to do by default when you call #cache_key.

With Rails 5.2 this no longer works as you expect. Instead, it’s simply [class]/[id] -the timestamp is dropped.

The basic idea behind it is that the cache object can be updated directly; and thus cache keys can be reused, dramatically lowering the quantity of cache garbage and increasing the amount of useful objects stored in the cache; which should let you see a significant performance hit by increasing your successful cache hits.

The only drawback is that the cache needs to be updated using the new cache API – if you don’t do this, you will suddenly see that your cached objects no longer ‘automatically’ expire.

Whilst I do recommend upgrading your codebase to use the new cache API, you can disable it and return to the older #cache_key style (using update_at timestamp) by simply adding this to your config/environments environment file:

config.active_record.cache_versioning = false

This should get you working in the old way – I will write a post on how to maximise use of the new Rails cache API in a future post.

Changes to ActiveRecord::Dirty in Rails 5.2

A while ago I wrote a small post on how to manually mark an ActiveRecord model attribute as dirty. Since Rails 5.2 has some fundamental changes to ActiveRecord::Dirty.

Specifically, three methods have been deprecated and changed in 5.2. These are:

  • attribute_changed?
  • changes, and
  • previous_changes

Using these methods in a Rails 5.2 application will now result in the following message (or a variation of it) being outputted in your logs:

DEPRECATION WARNING: The behavior of `attribute_changed?` inside of after callbacks will be changing in the next version of Rails. The new return value will reflect the behavior of calling the method after `save` returned (e.g. the opposite of what it returns now). To maintain the current behavior, use `saved_change_to_attribute?` instead.

attribute_changed? and changed? remain the same when called within a before_save callback. But when used in an after callback there are new methods you should use instead, which the Rails Core Team believe should reduce code ambiguity of changed? before saving to the database: will_save_change_to_attribute? and has_changes_to_save?

In summary, if upgrading to Rails 5.2 and after modifying an object and after saving to the database, or within after_save:

  • attribute_changed? should now be saved_change_to_attribute?
  • changed? should now be saved_changes?
  • changes should now be saved_changes
  • previous_changes has no replacement, since the behavior for it changes.

And (optional; less ambiguity, more readable, but longer) after modifying an object and before saving to the database, or within before_save:

  • attribute_changed? should now be will_save_change_to_attribute?
  • changed? should now be has_changes_to_save?
  • changes should now be changes_to_save