Skip to content
fiedl edited this page Sep 20, 2014 · 4 revisions

General Caching

As an introduction, please read the guide Caching with Rails: An overview.

Since the application view heavily depend on the current user, we do not use page caching. Instead we user fragment caching and, in particular, model caching.

Since we often do several deployments per day and perform one to two machine reboots per week, we use a file store rather than having the cache in memory. Otherwise, we would have to rebuild the entire cache after each deployment or restart, which could take several hours, in which the application would suffer from timeouts.

Model Caching: Old Mechanism

The basic idea of model caching is to cache time-expensive methods instead of executing the expensive code on every call.

Please note, the following example represents our previous caching mechanism in order to describe the basic ideas of model caching. If you are looking for the current way to do it, please refer to the sections below.

class Foo
  def expensive_method
    # expensive calculation, which could take several seconds of queries and processing.
  end
  def cached_expensive_method
    Rails.cache.fetch([self, 'expensive_method'], expires_in: 1.week) { expensive_method }
  end
  def delete_cached_expensive_method
    Rails.cache.delete [self, 'expensive_method']
  end

  def fill_cache
    cached_expensive_method
    # ...
  end
  def delete_cache
    delete_cached_expensive_method
    # ...
  end
end

# From outside:
foo.expensive_method         # uncached version
foo.cached_expensive_method  # cached version

Caching in This Application: New ways to do the same

There are two new ways to do the same: In variant 1, the instance method takes the responsibility to cache. In variant 2, the caller of the instance method, which in most cases would be a helper or a view, is responsible for requesting the cached version.

Both variants have their use cases, but variant 1 is recommended.

Variant 1: The expensive method introduces caching by default (recommended)

class Foo
  def expensive_method
    cached do
      # expensive calculation, which could take several seconds of queries and processing.
    end
  end

  def fill_cache
    expensive_method
    # ...
  end
end

# From outside:
foo.expensive_method                           # cached version
foo.cached(:expensive_method)                  # cached version
foo.uncached(:expensive_method)                # uncached version
Rails.cache.uncached { foo.expensive_method }  # uncached version

Variant 2: The expensive method is not responsible for caching

class Foo
  def expensive_method
    # expensive calculation, which could take several seconds of queries and processing.
  end

  def fill_cache
    cached(:expensive_method)
    # ...
  end
end

# From outside:
foo.expensive_method                           # uncached version
foo.cached(:expensive_method)                  # cached version
foo.uncached(:expensive_method)                # uncached version
Rails.cache.uncached { foo.expensive_method }  # uncached version

Under the hood

Watch out!

  • The cache invalidation takes at least one second, since the updated_at column is part of the cache key. In specs, use the wait_for_cache method if you run into problems. See section Writing Specs below.

  • If you introduce new caching (in both, variant 1 or 2), make sure to call the proper method in the fill_cache method of the concerning class. Otherwise, the cache won't be filled in the nightly rake task.

Advantages

  • Less code duplication when writing model caches.

    • No need to define a cached_expensive_method.
    • No need to define a delete_cached_expensive_method.
    • No need to include the cached method in the delete_cache method.
    • But one has to include the method to cache in the fill_cache method.
  • There are ways to make sure to get the cached or the uncached version:

    user.cached(:title)
    user.uncached(:title)
    Rails.cache.uncached { user.title }

Writing Specs

There are three points to watch out for in specs.

  1. The application cache is reset between separate specs.
  2. The automatic cache invalidation is only exact to whole seconds, because, internally, the updated_at column is used in the cache keys. If you suspect a spec to fail due to this point, do not use sleep 2 to wait for the cache to be invalidated. Instead use wait_for_cache, which simulates time elapse using the Timecop gem.
  3. Make sure to test if the cache is properly invalidated when dependent data has changed. For example:
group = create :group
user = create :user

group.cached(:members)
group.members << user
wait_for_cache

group.cached(:members).should include user

Nightly Caching

We use a rake task at lib/tasks/cache.rake to fill expired and invalidated caches. This task calls the fill_cache method on several objects including all users, groups and memberships.

Manually, this task can be executed by calling bundle exec rake cache:all.

The task is automatically executed on a nightly basis.