The out-of-the-box strategy provided by Rails’ #fetch
method does the job for the majority of situations in an application. However, there are some situations where it doesn’t.
Unreliable services in the critical path
Sometimes you have to query a remote service in a critical path, such as rendering the page. An example of that is an A/B testing service which provides a JSON blob of configuration describing the tests to be run on the page. In order to render the page you need that JSON blob.
Rails’ standard strategy won’t work here because it makes the rendering of your page dependent on the availability and latency of the remote service providing that JSON blob. If it’s slow or unavailable when the cache expires then your page rendering is going to suddenly be slow or unavailable!
The Rails-interface-compatible solution to this is what I’ve been calling a “selfish cache” (I couldn’t find any existing public examples of this, so I had to come up with the name myself). The implementation in Ruby is less than a page:
def selfish_fetch(key, expires_in:)
must_expire_in = expires_in * 2
entry = Rails.cache.read(key, expires_in: must_expire_in)
expired = entry.nil? || entry.expired?
if expired
begin
value = yield
Rails.cache.write(
key,
SelfishEntry.new(value, expires_in),
expires_in: must_expire_in,
)
value
rescue => error
# Reraise if there's nothing to fall back to.
raise if entry.nil?
backend.write(key, entry.reset, expires_in: must_expire_in)
# Report to your exception handling here.
# Then return the previous value:
entry.value
end
else
entry.value
end
end
class SelfishEntry
attr_reader :value
def initialize(value, expires_in)
@value = value
@created_at = Time.now.to_f
@expires_in = expires_in.to_f
end
def expired?
@expires_in && @created_at + @expires_in <= Time.now.to_f
end
def reset
self.class.new(value, @expires_in)
end
end
Extremely slow recalculation
If recalculating the value is extremely expensive and it’s okay to have stale data: then return the stale data and recalculate in the background. That way the requester gets a fast, cached response, and the cache is kept made consistent with the source asynchronously. Implementing this is a bit more complex than the selfish cache, so over the course of my work I built the async_cache
Ruby gem to provide this functionality.