Monday, April 6

Using The Rails MemoryStore Cache to Store ActiveRecord Objects in Development Mode

Despite the length of the title up there, wanting to use the Rails cache to store an ActiveRecord object isn't something that's too advanced or unreasonable. It makes sense to store relatively static objects in some kind of cache to save a round trip to whatever is actually storing them proper; since that store is usually a database of some kind it's likely that these static objects will be based on AR seeing as that's the default ORM doohickey in Rails.

That said, I'm not actually using proper AR models. The data for my models is coming over the wire from a magical place that has no databases, so in theory I shouldn't be using AR anyway. However a lot of non-ORM good stuff comes with AR (like validations and other convention-based magic), and seeing how some clever chap went to the trouble of writing a plugin to allow us to use "tableless" AR models I couldn't help but oblige and use it.

But I digress. This post is about what happens when we try to cache said AR object using Rails' built in cache functionality (when the cache is based on the hash-like MemoryStore). In short, the answer is a terrible disaster of immense (and typically agile-like if I'm allowed to make such a dig) proportions. Here's the technical bit: any instance methods you define on your AR models get lost when you retrieve them from the cache.

What the heck? Well, let's create a simple example. We'll start with a model representing a person - one who has a first name and a surname as fields/columns in the database. AR is magical in that it will look at the DB and create accessors for first name and surname for us. However we want something slightly more than that and so we define a method that will return a concatenation of the two. We'll call this method name, but I'll spare you the code.

Now we only have ten people in the system, and since these are guaranteed not to change in the lifetime of the application it makes sense to cache them after retrieving them from the DB. Let's ignore the fact that we could define them in the application itself (in my opinion, data belongs in the DB no matter how static it is). So let's store it in the cache:


all_persons = Person.get_all_persons
all_persons[0].name #returns a concatenated name
Rails.cache.write("all_persons", all_persons)


where get_all_persons returns a simple array of persons after getting them from the DB.

A Rails request later, and instead of calling get_all_persons we use the following:


all_persons = Rails.cache.read("all_persons")


Et voila, we once again have an array of all persons.

Except when we try the following something goes wrong and:


all_persons[0].name


results in:


NoMethodError: undefined method `name' for #Person


But... but my source code says the method is still there. What's gone wrong?

The trouble stems from how Rails loads classes. There is another (magical, naturally) option which tells Rails to watch source files and reload classes if any of them change. This is handy for development, since it means you can make a change and see it without restarting the Rails server. The way in which Rails does this magic is even cleverer - it literally reaches into the code of the class and modifies the methods which it has on offer. Amazing, no?

Well maybe. See, it's this exact mechanism which first strips an AR class of all its instance methods (something put in to deal with a memory leak) and then reload them later with the same magic used to create accessor methods. Except, for some reason, it misses out the ones defined explicitly like name, above. Yes, I was just as bemused.

But let's get our story straight; there are a few red herrings and caveats here. Firstly, this has nothing to do with Rails.cache - that just acts as a mechanism to keep objects around long enough to have their classes redefined. New objects created in the lifetime of a request use the original, and so correct, class definitions - which is why name gets called correctly the first time. No, it's Rails' class reloading (or rather, redefining) mechanism which is at fault here, or rather the way it deals with reloading AR classes.

Knowing this much presents us with a few options. The first and most obvious is to disable class reloading - indeed non of these problems occur in the predefined test and production environments where class reloading is disabled (or rather class caching is enabled). This would make development a pain though and take away one of Rails' biggest pros: it's facilitating of rapid development.

The second solution is a bit more sneaky. As we've discussed, freshly created objects in a request are loaded fine, and so if we somehow recreated the objects in the cache (perhaps via a copy) and use those, everything would be hunky dory. There is actually a more straightforward way to do this, and that's to use Marshal to serialise objects coming in and out of the cache; unmarshalling creates new objects to load up the data into. In fact since other cache stores (memcached and filestore) already do this marshalling by default (since they have to, whereas MemoryStore can just hold objects, well, natively), we don't see this issue (albeit by accident).

There are drawbacks here. Marshalling brings with it an unnecessary overhead - the marshalling isn't actually required, we just want to use its implicit "newing" behaviour. This means that when you push the code to test and productions, we're not doing things as efficiently as we could be (remember, we don't reload classes in those environments).

For now, I'll be marshalling explicitly when using a MemoryStore cache. Perhaps if I move to another cache I'll remove this redundancy, but for now I'll leave it in if it means I get to keep Rails' reloading of classes. Oh and for more info on this behaviour, see here.

Oh and before I forget, yes, I am now using Ruby on Rails to further my project. This flies in the face of a previous post but I don't mind contradicting that. I've not looked back since moving to Rails, but more on that in a later post.

No comments:

Post a comment