Tuesday, August 07, 2007

Ruby and instance_eval

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:

Reading list