Lazy evaluation cannot be cached

This is part of the Semicolon&Sons Code Diary - consisting of lessons learned on the job. You're in the caching category.

Last Updated: 2024-11-21

I wrote the following code and it executed just fine. This was despite the fact that, as I would later learn, it had an error:

@taxonomies = Rails.cache.fetch("taxons_db_data_#{current_store}") do
  Taxonomy.includes(:taxon) # should have been :taxons (plural)
end

My first hint that there was a bug here was that downstream code consuming this @taxonomies instance variable failed - the bug essentially was deferred until this data structure was consumed. I saw the issue and fixed the incorrect pluralization:

@taxonomies = Rails.cache.fetch("taxons_db_data_#{current_store}") do
  Taxonomy.includes(:taxons) # now plural
end

Desite my fix, the bug continued. This seemed weird because usually a cache doesn't store errors - i.e. this would not be cached.

@taxonomies = Rails.cache.fetch("taxons_db_data_#{current_store}") do
  raise
end

When I experimented with clearing the cache, however, my code worked again, indicating that the cached entity was the source of my woes. And so I got to the bottom of it. The issue was that Rails scopes (i.e. code that executes SQL such as Taxonomy.include()) are lazy, so therefore the error doesn't actually crop up until you call .to_a (explicitly or implicitly) on it - which only happens upon consumption of the taxonomies array.

I can prove this laziness by looking at what the postgres server (not the Rails client logs, which cannot be fully trusted) see when I cache this way:

taxonomies = Rails.cache.fetch("test123") { Taxonomy.includes(:taxons) }
# postgres server: executes every time - i.e. even though it is correct, it is not cached.

taxonomies = Rails.cache.fetch("test123") { Taxonomy.includes(:taxons).to_a }
# postgres server: DOES NOT execute every time, i.e. it caches

Lessons