# UniCache is Universal purpose Cache with Least Recently Used # replacement policy by default. Cache can be configured with another # policy. # # UniCache is intended to be Thread safe. # # User can register callbacks that are run at various UniCache events. # ":getdata" callback is used to retreive cache data if not in # cache. Others do not effect the UniCache state. # # Usage example: # require 'unicache' # # # Create cache with size 2. # c = UniCache.new( 2 ) # # # Register callbacks for some events (two for hit). # c.registerCallback( :hit, Proc.new{ |k,v| puts "Hit: #{k}:#{v}" } ) # c.registerCallback( :hit, Proc.new{ |k,v| puts "Found: #{k}:#{v}" } ) # c.registerCallback( :miss, Proc.new{ |k,v| puts "Miss: #{k}:" } ) # # # Set some values. # c[0] = 'foo' # set 0 as 'foo' # c.put( 'bar', 'foo' ) # # # Get (or try to get) some values. # a = c[ 'bar' ] # hit, a == 'foo' # b = c.get( 0 ) # hit, b == 'foo' # c.get( 'foo' ) # miss # c.get( 2 ) # miss # # Produces: # Hit: bar:foo # Found: bar:foo # Hit: 0:foo # Found: 0:foo # Miss: foo: # Miss: 2: class UniCache < Hash require 'notifyhub' # Generic UniCache exception. class Error < StandardError; end # ":getdata" callback data fetch exception. class FetchError < Error; end # Entry remove error. class RemoveError < Error; end # Callback error. class CallbackError < Error; end # Sizing error. class SizeError < Error; end # Cache size. attr_reader :size # Access lock. attr_reader :lock # Cache initialization. # # @param size [Integer] Cache size. # @param evict [Object] Eviction policy. def initialize( size, evict = LruEviction.new ) super() @lock = Mutex.new resize( size ) setEviction( evict ) @cb = NotifyHub.declare( :getdata, :add, :replace, :put, :overwrite, :remove, :valueremove, :hit, :miss, :peek ) end # Set the eviction policy. # # @param evict [Object] Cache eviction policy. def setEviction( evict ) @evict = evict end alias :hash_set_op :[]= alias :hash_get_op :[] alias :hash_store :store alias :hash_fetch :fetch alias :hash_delete :delete # Disable Hash manipulate methods: [ :merge, :merge!, :delete_if, :replace, :keep_if, :reject, :update, :shift ].each do |method| define_method( method ) {} undef_method method end # Dummy definition for "store" just for removal. def store(); end # Remove "store" from UniCache. remove_method :store # Put new entry to the Cache. Make space for the new entry if # needed. # # @param key [Object] Key (or tag) of the Cache entry. # @param value [Object] Value of the Cache entry. def put( key, value ) @lock.lock ret = _put( key, value ) @lock.unlock ret end # Put operator. def []=( key, value ) self.put( key, value ) end # Get Cache entry by key or return nil if not in Cache. # # If block is provided, it is run under UniCache lock and will # protect the used data from being removed (concurrently). # # If ":getdata" callback is defined, it is used to get data. Data # is always used, also when nil. UniCache lock is released before # callback is called. # # @param key [Object] Key (or tag) of the Cache entry. # @yield Procedure to fetch the data. def get( key, &blk ) @lock.lock ret = _get( key, &blk ) @lock.unlock ret end # Get operator. def []( key ) get( key ) end # Get Cache entry by key (no effect to eviction). # # @param key [Object] Entry key or tag. def peek( key ) runCallbacks( :peek, key ) hash_fetch( key, nil ) end # Get Cache entry existance by key (no effect to eviction). Uses # "peek", i.e. peek CB is run. # # @param key [Object] Entry key or tag. def exist?( key ) if peek( key ) true else false end end # Remove oldest Cache entry (Least Recently Used) or given. # # @param key [Object] Key to remove. # @return [Object, Object] Key/value pair removed or nil if no entries. def remove( key = nil ) @lock.lock ret = _remove( key ) @lock.unlock ret end # Resize the Cache. # # @param size [Integer] New Cache size. def resize( size ) raise SizeError, "UniCache: Size must be bigger than 0" unless size > 0 @lock.lock # Remove entries that does not fit anymore. if @size && size < @size ( @size - size ).times do _remove end end ret = @size = size @lock.unlock ret end # Register a callback to be executed on Cache event. # # Possible events: # # [getdata] Data not found in cache, callback is used to fetch # it. Data is always added to cache unless an exception # is raised. Also all retrys should be captured into # ":getdata". from request. # # [add] Item is added to Cache without anything being # replaced. from request. # # [replace] Item is added to Cache and another item is # removed. is evicted entry. # # [put] Item is added to Cache. Cache conditions has no effect # i.e. always run for "put". from request. # # [overwrite] Existing entry value is replaced by new item # value. from request. # # [remove] Entry is deleted from Cache. # # [valueremove] Entry value is removed (remove or overwrite). # from old entry. # # [hit] Entry is found in Cache. from cache. # # [miss] Entry is not found in Cache. from request. # # [peek] Cache peek. from request. # # Example: # registerCallback( :add, # Proc.new do |k,v,o| puts "Key: #{k} with value #{v} added" end # # @param type [Symbol] Callback event. # @param proc [Proc] Callback called with args: , , . def registerCallback( type, proc ) begin @cb[ type ].action( &proc ) rescue raise CallbackError, "UniCache: Trying to register invalid callback..." end end # Remove all callbacks for type or all. # # @param type [Symbol] Remove callback by type. def removeCallback( type = nil ) if type @cb.remove( type ) else @cb.ids.each do |id| @cb[id].remove end end end # Clear all Cache entries. def clear @lock.lock self.each do |k,v| removeEntry( k ) end @evict.clear @lock.unlock end # LeastRecentlyUsed eviction policy. Mutex should be used to # ensure atomicity of operations. # # Eviction policy class should include methods: # [update] Called when key is accessed (get/put). # [remove] Called when key is removed. # [nextEvict] Peek to what is going to be removed next. # [clear] Reset eviction state. class LruEviction # Instantiate. def initialize @lock = Mutex.new clear end # Clear eviction list. def clear @lock.lock @list = [] @lock.unlock end # Keep track of the least recently used keys. Place oldest at # the beginning of the list. # # @param key [Object] def update( key ) @lock.lock # Delete old entries of this key. @list.delete_if do |i| i == key end # Add to end of LRU. @list.push key @lock.unlock end # Remove oldest entry. # # @param key [Object] If given, remove this entry instead of oldest. def remove( key = nil ) @lock.lock res = nil if key @list.delete_if do |i| i == key end res = key else res = @list.shift end @lock.unlock res end # Return the oldest i.e. next to be evicted. def nextEvict @list[0] end end private def _put( key, value ) runAdd = false if hash_fetch( key, nil ) # Entry exists already. runCallbacks( :overwrite, key, value ) runCallbacks( :valueremove, key ) elsif self.length >= @size # Overflow. runCallbacks( :replace, @evict.nextEvict ) _remove else # Delay add callback to have the entry value set. runAdd = true end @evict.update( key ) hash_store( key, value ) runCallbacks( :add, key ) if runAdd runCallbacks( :put, key ) self end def _get( key, &blk ) ret = hash_fetch( key, nil ) if ret @evict.update( key ) runCallbacks( :hit, key ) else runCallbacks( :miss, key, nil ) if @cb[ :getdata ][0] # Get value using callback. @lock.unlock if @lock.locked? ret = runCallbacks( :getdata, key, nil ) @lock.lock _put( key, ret ) end end if block_given? # Get value using block. yield( ret ) else ret end end def _remove( key = nil ) if key unless keys.index( key ) @lock.unlock if @lock.locked? raise RemoveError, "Key \"#{key}\" does not exist..." end else key = @evict.remove end value = removeEntry( key ) [ key, value ] end # Remove entry from Cache. def removeEntry( key ) if hash_fetch( key, nil ) runCallbacks( :remove, key ) runCallbacks( :valueremove, key ) value = hash_delete( key ) else nil end end # Run all callbacks per type. def runCallbacks( type, key, value = nil ) unless value value = hash_fetch( key, nil ) end @cb[ type ].notify( key, value, self ) end end