lib/rdf/util/cache.rb
module RDF; module Util
##
# A `Hash`-like cache that holds only weak references to the values it
# caches, meaning that values contained in the cache can be garbage
# collected. This allows the cache to dynamically adjust to changing
# memory conditions, caching more objects when memory is plentiful, but
# evicting most objects if memory pressure increases to the point of
# scarcity.
#
# While this cache is something of an internal implementation detail of
# RDF.rb, some external libraries do currently make use of it as well,
# including [SPARQL](https://github.com/ruby-rdf/sparql/) and
# [Spira](https://github.com/ruby-rdf/spira). Do be sure to include any changes
# here in the RDF.rb changelog.
#
# @see RDF::URI.intern
# @see http://en.wikipedia.org/wiki/Weak_reference
# @since 0.2.0
class Cache
# The configured cache capacity.
attr_reader :capacity
##
# @private
def self.new(*args)
# JRuby doesn't support `ObjectSpace#_id2ref` unless the `-X+O`
# startup option is given. In addition, ObjectSpaceCache is very slow
# on Rubinius. On those platforms we'll default to using
# the WeakRef-based cache:
if RUBY_PLATFORM == 'java' || (defined?(RUBY_ENGINE) && RUBY_ENGINE == 'rbx')
klass = WeakRefCache
else
klass = ObjectSpaceCache
end
cache = klass.allocate
cache.send(:initialize, *args)
cache
end
##
# @param [Integer] capacity
def initialize(capacity = nil)
@capacity = capacity || RDF.config.cache_size
@cache ||= {}
@index ||= {}
end
##
# @return [Integer]
def size
@cache.size
end
##
# @return [Boolean]
def capacity?
@capacity.equal?(-1) || @capacity > @cache.size
end
alias_method :has_capacity?, :capacity?
##
# This implementation relies on `ObjectSpace#_id2ref` and performs
# optimally on Ruby >= 2.x; however, it does not work on JRuby
# by default since much `ObjectSpace` functionality on that platform is
# disabled unless the `-X+O` startup option is given.
#
# @see http://ruby-doc.org/core-2.2.2/ObjectSpace.html
# @see http://ruby-doc.org/stdlib-2.2.0/libdoc/weakref/rdoc/WeakRef.html
class ObjectSpaceCache < Cache
##
# @param [Object] key
# @return [Object]
def [](key)
if value_id = @cache[key]
ObjectSpace._id2ref(value_id) rescue nil
end
end
##
# @param [Object] key
# @param [Object] value
# @return [Object]
def []=(key, value)
if capacity?
id = value.__id__
@cache[key] = id
@index[id] = key
ObjectSpace.define_finalizer(value, finalizer_proc)
end
value
end
##
# Remove cache entry for key
#
# @param [Object] key
# @return [Object] the previously referenced object
def delete(key)
id = @cache[key]
@cache.delete(key)
@index.delete(id) if id
end
private
def finalizer_proc
proc { |id| @cache.delete(@index.delete(id)) }
end
end # ObjectSpaceCache
##
# This implementation uses the `WeakRef` class from Ruby's standard
# library, and provides adequate performance on JRuby and on Ruby 3.x.
#
# @see http://ruby-doc.org/stdlib-2.2.0/libdoc/weakref/rdoc/WeakRef.html
class WeakRefCache < Cache
##
# @param [Integer] capacity
def initialize(capacity = nil)
require 'weakref' unless defined?(::WeakRef)
super
end
##
# @param [Object] key
# @return [Object]
def [](key)
if (ref = @cache[key])
if ref.weakref_alive?
ref.__getobj__ rescue nil
else
@cache.delete(key)
nil
end
end
end
##
# @param [Object] key
# @param [Object] value
# @return [Object]
def []=(key, value)
if capacity?
@cache[key] = WeakRef.new(value)
end
value
end
##
# Remove cache entry for key
#
# @param [Object] key
# @return [Object] the previously referenced object
def delete(key)
ref = @cache.delete(key)
ref.__getobj__ rescue nil
end
end # WeakRefCache
end # Cache
end; end # RDF::Util