rails: Rails.cache.fetch unable to retrieve previous cached value if expired

Steps to reproduce

key = 'foo'
args = { expires_in: 10 }
Rails.cache.fetch(key, args) { 'bing' }
sleep(10)
entry = Rails.cache.send(:read_entry, key, args)
begin
  Rails.cache.fetch(key, expires_in: 10) { raise 'error' }
rescue StandardError
  # nice to have to reset original cache value
  Rails.cache.write(key, entry.value, args) if entry&.expired? # write back into the cache the original value and reset cache 
  entry&.value
end

Problem statement

If an unexpected error occurs within the yield block say to a service call being down for a short period, it would be nice to provide the option to retrieve the existing cached value and re inject the value back into the cache to check again when the new cache period expires. This would allow servers to continue to function with a value rather than crashing because a consuming service went down for retrieving updates making my server more resilient to other consuming service problems.

Actual behavior

If the yield block fails, the previous cached value is deleted and no longer accessible after the fact.

Enhancement request

My suggestion is to move read_entry to be publicly accessible allowing consumers to first hold the previous result prior to calling the fetch. If the fetch yields an error, the consumer can then catch their acceptable errors, determine their acceptable retries for exponential backoff, and then write back into the cache their wait period for attempting the ‘refresh’ of their cached content.

System configuration

Rails version: 5.2 or greater Ruby version: 2.6 or greater

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 18 (10 by maintainers)

Commits related to this issue

Most upvoted comments

Wait, what am I saying? You’d just need to use Rails Cache without an expiration:

value, cached_at = Rails.cache.fetch('cachedthing') do # don't set expires_in: 10.minutes here
  ['This is my cached value', Time.now]
end

if !cached_at || cached_at < 10.minutes.ago
  # enqueue an ActiveJob to fetch the new value and write it into the cache
end

return value

Alternatively, you could do:

Rails.cache.fetch('cachedthing') do # don't set expires_in: 10.minutes here
  MyCacheJob.set(wait: 10.minutes).perform_later # enqueue a job to rebuild the cache in 10 minutes
  'This is my cached value'
end