Saturday, August 7, 2010

Rails 2.3 ActiveSupport::Cache::MemoryStore Freezes Objects

Using MemoryStore for caching in Rails 2.3.X has what I would consider a show-stopper bug. When you write an object to the cache using Rails.cache.write, the original object gets frozen. When you read that object from the cache, it is also frozen. So, if you do something like read/write-through a cache for all your ActiveRecord models, you'll never be able to modify any instances of your models in your code.

This same behavior is not present in other cache storage classes such as ActiveSupport::Cache::MemCacheStore. I don't think I'd ever use MemoryStore in production...but it is great for testing. When running all of my rspecs and cucumber features, I have those environments set to use MemoryStore. The problem is, many of my tests break when they try to modify objects that were frozen by the cache.

Here's a simple example demonstrating this. Look at the WTFs on lines 10 and 14 of the output below:


The Attempted Fix
It turns out this bug was filed (and partially fixed) a long time ago. Rails ticket #2655 was filed back in May 2009 about this exact issue. Yehuda Katz and Carl Lerche from EngineYard committed a fix for this (to duplicate the object into the cache instead of freezing it) on July 1, 2009.

This is great. It fixes line 10 in the example output above. However, it is not quite enough if you are putting ActiveRecord::Base objects into the cache because the object's @attributes hash needs to be dup'd too. It turns out, the "carlhuda" pair was well aware of this and committed a fix for that too, just moments before the other one.

Another problem still remains though. Their fix still involves freezing the duplicate object in the cache and directly returning that frozen object. We need one more change to the read method to duplicate the object on the way out too. I made such a fix and updated the test_store_objects_should_be_immutable test for MemoryStore to be consistent with that of MemCacheStore

The final problem is that carlhuda's commits never got merged into the Rails 2.3 branch. A quick look at the github network graph for the rails project on July 1, 2009 tells the story.

Click Image To Enlarge

That yellow branch looks like wycats and carllerche were pairing on a bunch of bug fixes around this time. Notably the two short red arrows I drew here show that they cherry-picked two of those and merged them into the 2-3-stable branch. Then, shortly after that, they make a few more fixes that never got merged into the 2-3-stable branch. The two I circled in red are the two fixes I mentioned above.

Completing The Fix
I don't know if the Rails team is planning any more updates to the 2.3 line, but if they do release a 2.3.9, I vote for including these two commits, plus the one I applied. So, I forked the repo and created a topic branch off of 2-3-stable that cherry-picks the two carlhuda commits and includes my additional commit.

A Workaround
Since I'd rather not maintain my own patched version of Rails, for now I am just monkey-patching MemoryStore and ActiveRecord::Base like so:

3 comments:

David Stamm said...

Thanks for the monkeypatch, and even more for the clear explanation of its purpose and provenance!

My team encounters this issue intermittently with MemCacheStore. Trying to modify an AR object fetched with cache results in baffling "Can't modify frozen hash" errors. Code that runs fine in the console fails when used in a Rails controller.

We fixed the majority of these issues by using the ActiveRecord::Base#dup patch you presented. (We found it at http://is.gd/epSTo) But the frozen hash issues keeps coming up in random edge cases. It is approaching "deal-breaker" status for us.

Curiously, the 2nd patch you present in this article fixes the most easily reproduced of these errors. Even though it is a patch for ActiveSupport::Cache::MemoryStore and not for MemCacheStore! Does MemCacheStore delegate certain logic to MemoryStore, or something wacky like that?

scottwb said...

Glad the patches helped. I'd have a hard time believing that MemCacheStore delegated anything to MemoryStore from my cursory understanding of the code...but I could be missing something. From what I've seen, when using MemCacheStore, objects written to the cache are marshaled with Marshal.dump and when read, are loaded back via Marshal.load. I had actually contemplated using that instead of `dup` in the above patches just to make it even more certain to behave the same with MemoryStore in my tests as it does with MemCacheStore in production.

I wonder if maybe something above MemCacheStore is freezing the objects you read from the cache. I'd be interested to hear back if you figure out what that is...

David Stamm said...

The "can't modify frozen hash" error no longer happens with these patches. But another insidious bug remains...

Intermittently, attempts to update attributes on an ActiveRecord object fetched from cache will fail silently. Certain fields get updated, and others don't. This issue comes and goes, never happens in the console, and never happens to AR objects fetched from the DB.

Because of this issue, we actually had to ship a 1.0 release that made significantly less use of memcached than we would have liked. Very frustrating!