Ognjen Regoje bio photo

MY NAME IS
Ognjen Regoje
BUT YOU CAN CALL ME OGGY


I make things that run on the web (mostly).
More /ABOUT me.

me@ognjen.io Twitter LinkedIn Instagram Github

Using multiple updated_at timestamps for caching in Rails

It sounds so obvious when spelled out but you can use multiple timestamps in Rails models that each individually can serve as cache keys for different views. For example say you have an Article that looks like this:

# app/models/article.rb

# == Schema Information
#
# Table name: articles
#
#  id                          :integer          not null, primary key
#  title                       :string(255)
#  content                     :text(16777215)
#  number_of_views             :integer
#  created_at                  :datetime         not null
#  updated_at                  :datetime         not null

class Article < ActiveRecord::Base
end

You’ll most likely have a partial for the article:

# app/views/articles/_article.html.erb

<%= cache article do%>
  <h2><%= article.title%></h2>
  <%= article.content%>
<%end%>

and you’re incrementing the number_of_views in a background job with some kind of rate limiting or debouncing doing something like:

article.number_of_views = article.number_of_views + debounced_count
article.save

So, pointing out the obvious is that that’s invalidating the cached partial above even though the number_of_views data is not used in the partial itself. This is happening because if .save persists changes to a model it sets the updated_at property of that model to the current datetime – as you would expect. Furthermore, by defauly rails uses updated_at as part of it’s cache key.

I’ve mitigated this in two ways. First, for models that are ‘small’ you can use update_columns like this:

article.update_columns({number_of_views: article.number_of_views + debounced_count})

update_columns does not trigger callbacks, validations or increments to updated_at.

Second, in cases where there are two separate fragments that need to be cached. For instance, if you had the user partial and the admin partial. The admin is not really concerned with the content but just about the latest stats:

# app/views/articles/_article.html.erb

<%= cache [:article, article.id, article.user_updated_at] do%>
  <h2><%= article.title%></h2>
  <%= article.content%>
<%end%>


# app/views/admin/articles/_article.html.erb

<%= cache [:article, article.id, article.admin_updated_at] do%>
  <h2><%= article.title%></h2>
  <%= article.content%>
<%end%>

In this scenario you have to maintain the two separate timestamps but that’s easy enough:

#... snip ...

class Article < ActiveRecord::Base
  before_save do
    if title_changed? or content_changed?
      self.client_updated_at = DateTime.now
    end

    if number_of_views_changed?
      self.admin_updated_at = DateTime.now
    end
  end
end

You can then get super fancy by creating a hash of which properties trigger changes to which timestamps and then using self.changes to get a full list of attributes changed. Based on that list you can look up which timestamps to update. Finally, roll all of that into a nice concern and you can have it everywhere with minimal effort.

The scenario above, of course, is an over simplification but it does illustrate a point. Imagine if the article contains embedded generated graphics, or content from other data structures. If you start getting a large number of hits your cache will constantly be invalidated and your servers will constantly have to regenerate the graphic and hit the database for the other data.

#caching #rails #technical