-
Notifications
You must be signed in to change notification settings - Fork 13
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.
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
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.
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
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
-
The cache invalidation takes at least one second, since the
updated_at
column is part of the cache key. In specs, use thewait_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.
-
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.
- No need to define a
-
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 }
There are three points to watch out for in specs.
- The application cache is reset between separate specs.
- 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 usesleep 2
to wait for the cache to be invalidated. Instead usewait_for_cache
, which simulates time elapse using the Timecop gem. - 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
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.