Volatile memoization for Marshalled AR objects
The Ruby memcache-client gem uses the Marshal library to serialize ruby objects for storage in Memcached.
By default, all instance variables is marshalled as well, which pretty much defeats instance variable based memoization eg.
def to_param; @to_param ||= "#{self.id}-#{self.name.to_url}" end
Usage scenario
An account based application where almost all Models belongs to an account:
class Account < ActiveRecord::Base
acts_as_cached :version => 1, :include => [:subscription, :setting, :logo, :domain, :active_theme], :ttl => 1.day
belongs_to :domain
has_many :bookings, :class_name => 'PropertyBooking', :include => [:date, :customer], :foreign_key => :account_id, :dependent => :delete_all, :order => 'bookings.created_at DESC', :extend => BookingsExtension
#define other relationships
end
We set the account instance in a before filter for every request:
class AccountController < ApplicationController
attr_accessor :account
before_filter :account_from_subdomain
protected
def account_from_subdomain
@account = Account.get_cache([account_subdomain(),request.domain].join('.'))
if @account.nil?
account_not_found
else
@account.disabled? ? account_disabled() : account_found()
end
end
end
From time to time we need to reference the account belongs_to association to load/query other data scoped to the particular account:
class Booking < ActiveRecord::Base
#associations etc.
#verify subscription limit before saving another booking
def before_create; self.account.subscription.bookings_this_cycle? end
end
Headaches
Simply caching an account alongside a child Model forces us to implement excessive sweepers to preserve data integrity and also duplicates data, which isn't desireable:
class Booking < ActiveRecord::Base
#cache account alongside this model
acts_as_cached :version => 1, :include => [:account, :date, :progress, :payment], :ttl => 2.hours
end
We can override the reader/accessor method, but this results in excessive Memcache lookups if the related model is referenced multiple times per request:
class Booking < ActiveRecord::Base
#do not cache the account alongside this model
acts_as_cached :version => 1, :include => [:date, :progress, :payment], :ttl => 2.hours
#potential multiple lookups
def account; Account.get_cache(account_id) unless account_id.nil? end
end
The solution
We can use the after_initialize callback to clear an instance variable used for memoization:
class Booking < ActiveRecord::Base
#do not cache the account alongside this model
acts_as_cached :version => 1, :include => [:date, :progress, :payment], :ttl => 2.hours
#memoization, only called once per lifecycle/request
def account; @cached_account ||= Account.get_cache(account_id) unless account_id.nil? end
#AR callback
def after_initialize; @cached_account = nil end
end
In theory either afterinitialize OR afterfind can be used to accomplish this.
You need to ensure your cache backend reference these callbacks to mimick and replicate the standard ActiveRecord behaviour.
I use the following snippet for cache_fu:
module ActsAsCached
module ClassMethods
def fetch_cache(id)
return if ActsAsCached.config[:skip_gets]
autoload_missing_constants do
data = cache_store(:get, cache_key(id))
[:after_initialize, :after_find].each{|c| data.send(c) if data.respond_to? c }
data
end
end
end
end
memcache-client can be monkey patched if you rolled your own store using the CACHE.get API method:
class MemCache
def get(key, raw = false)
server, cache_key = request_setup key
value = if @multithread then
threadsafe_cache_get server, cache_key
else
cache_get server, cache_key
end
return nil if value.nil?
unless raw
value = Marshal.load value
[:after_initialize, :after_find].each{|c| value.send(c) if value.respond_to? c }
end
return value
rescue TypeError, SocketError, SystemCallError, IOError => err
handle_error server, err
end
end
Alternatives
The Marshal library define callbacks for more complex marshalling:
class SomeModel < ActiveRecord::Base
#prepare for serialization
def marshal_dump
end
#do something when deserialized
def marshal_load( serialized_string )
end
end
0 comments
Jump to comment form | comments rss [?]