Home > Ruby On Rails > Musings on cache-money – Part I

Musings on cache-money – Part I

So, I always wanted to find a way of memcaching via ActiveRecord, without having to re-invent the wheel 😉 My investigations initially took me via CachedModel, cache_fu and finally I settled on cache-money. Seems to be *almost exactly* what I wanted – any lookup goes via memcache, any update /edit goes via ActiveRecord + memcache and then if needed to the database.

This is the first of my stunts:

1. Create a basic rails project with a simple posts controller. Being as lazy as I am, I used the ./script/generate scaffold help contents to create my Posts controller! 😉

2. Install the cache-money gem

3. Install memcachd server and configure the rails project (all from the github README of cache-money)

— config/memcache.yml —

production:
    ttl: 604800
    namespace: 'josh1'
    sessions: false
    debug: false
    servers: localhost:11211

— environments/production.rb —

# Use a different cache store in production
config.cache_store = :mem_cache_store
memcache_options = {
   :c_threshold => 10000,
   :compression => true,
   :debug => false,
   :namespace => 'josh1',
   :readonly => false,
 :urlencode => false
}

# require the new gem, this will load up latest memcache
# instead of using the built in 1.5.0
require 'memcache'

# make a CACHE global to use in your controllers instead of
# Rails.cache, this will use the new memcache-client 1.7.2
CACHE = MemCache.new memcache_options

# connect to your server that you started earlier
CACHE.servers = '127.0.0.1:11211'

# this is where you deal with passenger's forking
begin
 PhusionPassenger.on_event(:starting_worker_process) do |forked|
 if forked
 # We're in smart spawning mode, so...
 # Close duplicated memcached connections - they will open themselves
 CACHE.reset
 end
end

# In case you're not running under Passenger (i.e. devmode with mongrel)
rescue NameError => error
end

— config/initializers/cache_money.rb

require 'cache_money'

config = YAML.load(IO.read(File.join(RAILS_ROOT, "config",
                                     "memcached.yml")))[RAILS_ENV]
$memcache = MemCache.new(config)
$memcache.servers = config['servers']

$local = Cash::Local.new($memcache)
$lock = Cash::Lock.new($memcache)
$cache = Cash::Transactional.new($local, $lock)

class ActiveRecord::Base
 is_cached :repository => $cache
end

4. Benchmarking – Instead of running benchmarking, I wrote a few lines of code myself to create a 1000 posts with random text ranging from 1 to 100000 letters.

Setup: Mac OS 1.5 (Leopard) with nginx + passenger + memcached on a MacBook Pro laptop. I used Ruby 1.8.6 (default Mac OS 1.5 Version) and Rails 2.3.3

[term1] $ memcached -uroot -vv

$ ./script/console production
Loading production environment (Rails 2.3.3)
>>  alphanumerics = [('0'..'9'),('A'..'Z'),('a'..'z')].map {
?>       |range| range.to_a}.flatten
=> ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D",
"E","F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S",
"T", "U", "V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h",
"i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w",
"x", "y", "z"]
>>  1000.times do |i|
?>    Post.create(:title => i.to_s, :body => (0...100000).map {
?>      alphanumerics[Kernel.rand(alphanumerics.size)] }.join)
>>  end

Results & Analysis

1. Memcache quickly scaled upto 64MB (default setting) which was cool. I could see the memcache screen scrolling fast to update all objects – in reality it will tries an LRU style purging and gets rid of the oldest objects once its actual memory is ‘full’. Memcache did not crash or stall or thrash — which was great!

2. For each object created in cache, we see the following memcache update:

<22 add lock/Post:1/id/15 0 30 6
>22 STORED
<22 set Post:1/id/15 0 86400 280
>22 STORED
<22 delete lock/Post:1/id/15 0
>22 DELETED

A GET for any posts results in:

<23 get Post:1/id/15
>23 sending key Post:1/id/15
>23 END

An UPDATE / CREATE / DELETE for any posts results in:

<23 add lock/Post:1/id/15 0 30 6
>23 STORED
<23 set Post:1/id/15 0 86400 361
>23 STORED
<23 delete lock/Post:1/id/15 0
>23 DELETED

Excellent!!

2. To confirm if we were getting it right, I checked the logs:

First time for GET:

Processing PostsController#show (for 127.0.0.1 at 2009-10-10 16:06:05) [GET]
 Parameters: {"id"=>"15"}
Rendering template within layouts/posts
Rendering posts/show
Completed in 5ms (View: 2, DB: 95) | 200 OK [http://josh1.local/posts/15]

Second Time for the same GET request:

Processing PostsController#edit (for 127.0.0.1 at 2009-10-10 16:06:42) [GET]
 Parameters: {"id"=>"15"}
Rendering template within layouts/posts
Rendering posts/edit
Completed in 6ms (View: 5, DB: 0) | 200 OK [http://josh1.local/posts/15/edit]

Excellent!!

2. Just to push the pedal, I ran another iteration of 1000 posts with random body text and saw that memcache memory stayed put at 62-64mb. I could see expired cache objects hitting the database and get cached and objects already in the cache NOT hitting the database. Exactly what I wished for.

Some caveats:

  1. a. Every ‘find’ request hits the database. Understandable but I wonder if this too can hit via ‘cache’ – its not safe or synch’ed but I wonder.
  2. money-cache still does not support joins or includes or nested attributes. (Time to contribute!! )

Overall: VERY VERY GOOD

Next Steps in Part II:

  • Test money-cache on a live project with about 1 million records.
  • It has currently ~35 Requests Per Minute.
  • Some controllers calls take almost 50% of the time. Gotta reduce that to 5% (hopefully)

Cheers!

  1. September 18, 2010 at 1:54 pm

    Hi Gautam,
    Excellent description of cache-money you have there. I am also evaluating for one of my projects…

    If I can ask.. did you tried this on engine yard?

  2. September 19, 2010 at 12:03 am

    No- we didn’t try this on EngineYard but I am sure it would be faster on REE.

    TODAY, I would recommend you use Rails Caching with :memcache as the RW memstore. 🙂

    • September 19, 2010 at 3:07 pm

      EY folks just introduced REE as a beta addon. I’m waiting till they take it out of beta…

  3. vasu
    September 23, 2010 at 6:53 pm

    Hi Gautam

    When I am trying to install cache-money gem it ask for active_support 1.8.7
    I am using ruby 1.8.6 for my application how you able to install it with Ruby 1.8.6

    • September 25, 2010 at 11:41 am

      Hi Vasu, You would probably require Ruby 1.8.7 — which btw is far more stable than 1.8.6.

      To be able to tweak CacheMoney you would have to edit gemspec — but there may be compatibility issues. Recommended appraoch – upgrade to 1.8.7 — its worth it anyway.

  1. No trackbacks yet.

Leave a reply to makuchaku Cancel reply