Thursday, August 28, 2008

InMemoryCacheStore

I really like the the acts_as_cached interface for model caching, but there are a few models that we wanted to cache in the process memory, but still maintain expiration functionality. We came up with this:

class InMemoryCacheStore < Hash

  def initialize(options={})
    @expire_entries = options[:expire_entries]
  end

  def set(key, data, ttl = nil)
    if @expire_entries && ttl
      self[key] = Entry.new(data.dup, Time.now.to_i + ttl) rescue data
    else
      self[key] = Entry.new(data.dup, nil) rescue data 
    end
  end

  # If we're expiring entires, first check the expiration and remove it if it's expired
  # key key
  def get(key)
    r = self[key]
    if @expire_entries && r && r.expires_at < Time.now.to_i
      self.delete(key)
      r = nil
    end
    r ? r.obj : nil
  end

private

  class Entry
    attr_accessor :obj        # cached object
    attr_accessor :expires_at # time in seconds

    def initialize(obj, expires_at)
      @obj = obj
      @expires_at = expires_at
    end
  end

end

Example without expiration:

class SomeModel < ActiveRecord::Base
  acts_as_cached :store => InMemoryCacheStore.new
end

Example with expiration:

class SomeModel < ActiveRecord::Base
  acts_as_cached :store => InMemoryCacheStore.new(:expire_entires => true)

  def self.ttl
    24.hours
  end
end

Performance for 1000 lookups: db on localhost: 1.52 secs memcached on localhost: 0.78 secs InMemoryCacheStore: 0.345 secs

Friday, August 22, 2008

Populating fixtures from a database table

One problem with the add_symbolic_names code posted earlier on the blog is the need to keep the test fixtures in sync with the data in the production database — otherwise, code can reference constants that exist in production, and will break while running tests. Keeping these in sync is annoying enough that I wrote a rake task to dump the contents of a table to the appropriate fixtures file. I ended up with something like this:
      desc "Overwrite a fixture file with the contents of a table (set by TABLE=)"
      task :generate_from_table do 
        ActiveRecord::Base.establish_connection(RAILS_ENV)
        table_name = ENV['TABLE']
        File.open("test/fixtures/#{table_name}.yml", 'w') do |file|
          data = ActiveRecord::Base.connection.select_all("SELECT * FROM #{table_name}")
          rows = {}
          
          data.each do |record|
            rows["#{table_name}_#{record['id']}"] = record
          end
          
          file.write rows.to_yaml
        end
      end

Running this rake task will generate a fixture file containing all the information in the table specified by the environment variable TABLE. In the future, we’ll probably look at automating the fixture generation for these symbolic tables so we don’t have to worry about failing tests when we add new rows. UPDATE: I realized that in most cases, the symbolic name of the database row makes a much better fixture title than the id, which results in the following code:
      desc "Overwrite a fixture file with the contents of a table (set by TABLE=)"
      task :generate_from_table do 
        ActiveRecord::Base.establish_connection(RAILS_ENV)
        table_name = ENV['TABLE']
        File.open("test/fixtures/#{table_name}.yml", 'w') do |file|
          data = ActiveRecord::Base.connection.select_all("SELECT * FROM #{table_name}")
          rows = {}
          
          data.each do |record|
            fixture_name = record['symbolic_name'].blank? ? 
              "#{table_name}_#{record['id']}" : 
              record['symbolic_name'].downcase
            rows[fixture_name] = record
          end
          
          file.write rows.to_yaml
        end
      end
      
This gives us nice fixtures that look like this:
--- 
dog: 
  name: Dog
  id: "1"
  symbolic_name: DOG
cat: 
  name: Cat
  id: "2"
  symbolic_name: CAT
  

Fixnum to Models

I saw andand a while back and lol'ed at how clever and awesome it was. With that in mind, we added a couple methods to Fixnum to clean up record fetching on our more popular models:
Fixnum.class_eval do

  def to_user
    User.find(self)
  end

end
That way when I find myself with something that might be a user id...
puts user_id.andand.to_user.andand.name
Maybe not something you'd want on every model, probably not optimized, but hey... it beats checking for nil, looking up the user id, checking for nil again and printing.