Tuesday, September 19, 2006

Home made cache with DRb

I recently came across the need to cache various Ruby objects in a persistent way. Memcached and memcached-client looked like the obvious solution, but I soon realised that Memcached isn't persistent -- it will start removing cached objects when it gets close to filling its allocated cache quota. I also wanted something that could be shared among servers and processes, as an Agile abiding developer I refused to violate the D.R.Y principle. So what other options did I have?

At this point I was getting a little worried that I was going to have to write my own distributed object system around Ruby's marshaling. A quick search around the web came up with a few prior implementations, but all were a lot more than I needed. I just wanted somewhere to store objects, no message bus, ACLs or events; just a simple store.

I soon came across DRb, Ruby's distributed computing library -- perfect! I'd heard of this library before, but never really looked into it or what it actually was. It turns out it was exaclty what I needed. It provides (as with most things Ruby) a dead easy way to transfer objects over a socket. All I had to write was my storage class, which is nothing more than a wrapper around a Hash.

In my case, the cache is for storing my parsed Liquid template objects.

The server:

require 'drb'

class TemplateStore

  def initialize

    @store = {}

  end

  def get(key)

    @store[key]

  end

  def put(key, value)

    @store[key] = value

  end

end

DRb.start_service("druby://:7777", TemplateCache.new)
DRb.thread.join

Run that tiny script and you have yourself a cache.

To connect to the cache:

DRb.start_service
template_cache = DRbObject.new(nil, 'druby://:7777')

You can now access template_cache as if it were a local instance of TemplateCache.

So with all the time the DRb has saved us, we can go a little further and add more services to our cache server. I also needed somewhere to store my registered Liquid drops (classes made available to the template). Notice that in our current example, the cache is bound to a specific port, we either need to open up another port for our drop registry or create a gateway mechanism that makes all of our services over a single port.

Example gateway:

class ServiceGateway

  def register_service(name, instance)

    instance_variable_set("@"+name, instance)

    self.class.instance_eval do

      define_method(name) do

        instance_variable_get("@"+name)

      end

    end

  end

end

and to startup the server:

gateway = ServiceGateway.new
gateway.register_service("template_cache", TemplateCache.new)
DRb.start_service("druby://:7777", gateway)
DRb.thread.join

and the client now uses the cache like this:


DRb.start_service
cache = DRbObject.new(nil, 'druby://:7777')
cache.template_cache.put(:my_key, "hello, world")

But wait a moment, this isn't going to work. Calling the put method of our TemplateCache instance will give us a NoMethodError. You need to include DRbUndumped into your TemplateCache class. This tells DRb not to marshal the instance returned to the client, but only a reference. Meaning that all methods called on the instance that comes out at the client end will be forwarded to the object on the server.

require 'drb'

class TemplateCache

  include DRbUndumped

  def initialize

    ...

So there you have it, a very simple distributed object cache in only a few lines of code.

No comments: