Tonight I learned a new trick in ruby.
Lets say you have a class that you would like to load with different behavior depending on the environment it's created in. Object inheritance can be used, but sometimes you want something a little more dynamic. Tonight I was trying to allow mongrel esi to work in a multithreaded environment and a single threaded environment to support viewing a new cache status page. I wanted to be able to share most of the code and avoid as much runtime conditional logic as possible. I also wanted to avoid having to duplicate a lot of function signatures over and over again, (get, get_unlocked, set, set_unlocked, etc...).
My first solution
# Wrapper around ruby Mutex to make it possible to enable or disable the Mutex object
# depending on whether or not ESI::Invalidator server is run, usually it runs on port 4000
class CacheLock
def initialize( options = {} )
if options[:locked]
@semaphore = Mutex.new
self.instance_eval do
def synchronize
@semaphore.synchronize {
yield
}
end
end
else
self.instance_eval do
def synchronize
yield
end
end
end
end
end
class Cache
def initialize( options = {} )
@cache = {}
@semaphore = CacheLock.new( options )
end
def cached?( uri, params )
@semaphore.synchronize {
fragment = @cache[cache_key(uri,params)]
fragment and fragment.valid?
}
end
def get( uri, params )
@semaphore.synchronize {
@cache[cache_key(uri,params)]
}
end
def put( uri, params, max_age, body )
@semaphore.synchronize {
@cache[cache_key(uri,params)] = Fragment.new(max_age,body)
}
sweep
end
# run through the cache and dump anything that has expired
def sweep
@semaphore.synchronize {
@cache.reject! {|k,v| !v.valid? }
}
end
def keys(&block)
@semaphore.synchronize {
@cache.each do|key,data|
yield key, data
end
}
end
def update_ttl( key, ttl )
@semaphore.synchronize {
update_ttl_unlocked( key, ttl )
}
end
def update_ttl_unlocked( key, ttl )
@cache[key] = Fragment.new( ttl, @cache[key].body )
end
private
def cache_key( uri, params )
http_x_requested_with = params['HTTP_X_REQUESTED_WITH'] || params["X-Requested-With"]
key = Digest::SHA1.hexdigest("#{uri}-#{http_x_requested_with}")
"#{uri}:%:#{key}"
end
end
A better solution
Okay, this works and it removed the burden of having conditional logic, but I still have the blocks all over my code and the repeated symaphore.synchronize in all my methods. Not to meantion I needed to define a few pesky method_name_unlocked for cases where I need to call a method instead of another and the method is already locked... I decided I should be able to redefine a select set of methods to include semaphore block when needed otherwise just alias_method to define the #{method}_unlocked versions of the methods. Talking through my issues on irc and at long last heres my solution:
# A hash table indexed by cache_key of Fragments.
# the cache is made thread safe if the external invalidator is active otherwise the Mutex is a no op
class Cache
def synchronize_methods( *methods_names )
methods_names.flatten.each do|name|
unlocked_name = "#{name}_unlocked".to_sym
locked_name = "#{name}_locked".to_sym
instance_eval do
def locked_name( *args )
@semaphore.synchronize {
send unlocked_name, args
}
end
end
end
end
def initialize( options = {} )
@cache = {}
synchronized_methods = [:get, :put, :put, :sweep, :keys, :update_ttl]
# define all the unlocked methods
synchronized_methods.each { |name| ESI::Cache.send :alias_method, "#{name}_unlocked", name }
if( options[:locked] )
@semaphore = Mutex.new
synchronize_methods synchronized_methods
end
end
def cached?( uri, params )
fragment = @cache[cache_key(uri,params)]
fragment and fragment.valid?
end
def get( uri, params )
@cache[cache_key(uri,params)]
end
def put( uri, params, max_age, body )
@cache[cache_key(uri,params)] = Fragment.new(max_age,body)
sweep_unlocked
end
# run through the cache and dump anything that has expired
def sweep
@cache.reject! {|k,v| !v.valid? }
end
def keys(&block)
@cache.each do|key,data|
yield key, data
end
end
def update_ttl( key, ttl )
@cache[key] = Fragment.new( ttl, @cache[key].body )
end
private
def cache_key( uri, params )
http_x_requested_with = params['HTTP_X_REQUESTED_WITH'] || params["X-Requested-With"]
key = Digest::SHA1.hexdigest("#{uri}-#{http_x_requested_with}")
"#{uri}:%:#{key}"
end
end

0 comments:
Post a Comment