At the Forge - Memcached Integration in Rails

Integrating memcached into your Rails application is easy and fast, with big benefits.

And, I point my Web browser to port 3000 on my server: http://atf.lerner.co.il:3000/people/.

So far, so good. With a few commands on the UNIX command line, I've managed to create a simple database of people. I'll use the scaffolded application to add several people, clicking on the New person link and then adding the first name, last name and e-mail address of each of my friends.

Now, if I look at the Rails development log, I easily can see that each act I perform from within the scaffolded environment results in an SQL query being built and sent to the PostgreSQL server. I often do this by typing:

tail -f log/development.log

For example, if I click on the show link for the first person I created, I see the following in the development log:

Person Load (0.001571)   SELECT * FROM "people" 
 ↪WHERE ("people"."id" = 1)

In other words, Rails knows that I want to load a Person object. It also knows that I retrieve such objects from the database. This is where ActiveRecord steps in, turning the Ruby:

Person.find(1)

into:

SELECT * FROM people WHERE people.id = 1

As you can imagine, it's not a big deal to do this sort of simple query, particularly if you have a limited number of fields, a small data set and a well-indexed primary key. But, as the number of fields increases, you might find yourself wanting to reduce the load on the database. Moreover, modern dynamic Web sites might need to retrieve 5–10 different objects from the database, only some of which are particular to the current user. If you get even 1,000 visitors to your site each day, and if there are three objects on each page that could be cached, that's 3,000 database queries you are foisting upon your database unnecessarily.

Memcached is an obvious solution to this problem. With previous versions of Rails, you needed to use a plugin or Ruby gem to do that. Now, however, you can do it via a configuration file. The gem that you previously needed to install, memcached-client, now is included along with the Rails gem. Every Rails application contains a main configuration file (config/environment.rb), which allows you to configure your application using Ruby code. This is where you should put configurations that are common to all three standard Rails environments: development, testing and production. For configurations that are specific to one environment, you instead would modify config/environments/ENV.rb, where ENV should be replaced with the environment of your choice.

Because we're still developing our example application, and using the development environment, we can confine our changes to config/environments/development.rb. Open that file in the editor of your choice, and add the following line:

config.cache_store = :mem_cache_store

This tells Rails that you want to use memcached and that the server is on the local computer (localhost), using the default port 11211. However, you can override these, and even put things into a separate namespace, if you're worried about stepping on someone else's objects.

When you're working in development mode, you also need to tell the server to use caching, a parameter that is set (and false) by default:

config.action_controller.perform_caching = true

Caching Objects

Now, let's go in and modify the GET action within the controller that was built for us by the scaffolding system. (The built-in caching is designed to be used from controllers and views, rather than from models.) That'll be:

app/controllers/people_controller.rb

On line 16 of that file, you'll see:

@person = Person.find(params[:id])

This is obviously where we invoke Person.find, as shown in the logs earlier. Now, modify that line so it looks like this:

@person = cache(['Person', params[:id]]) do
  Person.find(params[:id])
end

We still are assigning a value to @person. And, our call to Person.find is still in there. However, Person.find now is buried within a block. And, that block is attached to the call to a cache function, which is given an array argument.

What's happening here is actually fairly straightforward. The cache function looks in the cache for its argument, which is turned into a key. If a value for this key exists in the cache, the value is returned. If not, the block is executed, with the result of executing the block stored in the cache and returned to the caller.

With this code in place, let's retrieve person #1 again and look at the logfile. The first time we do this, the value is indeed retrieved from the database, as before:

Person Load (0.002212)   SELECT * FROM "people" 
 ↪WHERE ("people"."id" = 1)

That line is followed by this new entry:

Cache write (will save 0.01852): controller/Person/1

Sure enough, our memcached server reports:


<7 new client connection
<7 get controller/Person/1
>7 END
<7 set controller/Person/1 0 0 224
>7 STORED

In other words, our Rails controller did exactly as we asked. It contacted memcached and asked for the value of controller/Person/1. (We can see from this that controller is prefaced to the key name that we create, and that elements of the cache key array are separated by slashes.) When we get a null value back for that, Rails retrieves the value from the database and then issues a set command in memcached, storing our value.

As you might expect, we then can refresh our browser window and see that we are saving a great deal of database time by retrieving information about this person from the cache. So, we refresh the browser window, and...boom! Our application blows up on us, with an error message that looks like this:

undefined class/module Person

Now, the first time this happened to me, I wasn't sure what hit me. What do you mean, I asked my computer, you don't know how to find a Person class? A little head-scratching and Google searching later, and I found my answer. I needed to tell the controller to load the object definition by putting the following at the top of my controller:

require_dependency 'person'

This is apparently necessary only in development mode, and it has something to do with the way Rails reloads classes while you are developing your application. With that line in place, you can reload the page. In the logfile, you'll see no trace of a successful call to the database. Instead, you'll find the following:

Cache hit: controller/Person/1 ({})

Meanwhile, our memcached log will look like this:


<7 get controller/Person/1
>7 sending key controller/Person/1
>7 END

This is a good time to mention the only other gotcha I can think of: whitespace is forbidden in memcached keys. This can be a problem if you use a value from the database (for example, a parameter name) as the key when storing things in memcached. The simple solution is to remove the whitespace, either by running String#gsub on each of the keys or by monkey-patching String (as I did for an application I wrote) to add a to_key method. I could then pass "parameter name".to_mkey as an argument to cache().

______________________

Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

Thanks for the write up but

Anonymous's picture

Thanks for the write up but i seem to be having a bit of problem when i refresh my browser. I was expecting to see "undefined class/module Person" as stated but instead i get a different error message "undefined method `cache' for #

Good article.

bggy's picture

Thanks for the nice write-up!

Webinar
One Click, Universal Protection: Implementing Centralized Security Policies on Linux Systems

As Linux continues to play an ever increasing role in corporate data centers and institutions, ensuring the integrity and protection of these systems must be a priority. With 60% of the world's websites and an increasing share of organization's mission-critical workloads running on Linux, failing to stop malware and other advanced threats on Linux can increasingly impact an organization's reputation and bottom line.

Learn More

Sponsored by Bit9

Webinar
Linux Backup and Recovery Webinar

Most companies incorporate backup procedures for critical data, which can be restored quickly if a loss occurs. However, fewer companies are prepared for catastrophic system failures, in which they lose all data, the entire operating system, applications, settings, patches and more, reducing their system(s) to “bare metal.” After all, before data can be restored to a system, there must be a system to restore it to.

In this one hour webinar, learn how to enhance your existing backup strategies for better disaster recovery preparedness using Storix System Backup Administrator (SBAdmin), a highly flexible bare-metal recovery solution for UNIX and Linux systems.

Learn More

Sponsored by Storix