lib/active_support/cache.rb in activesupport-3.2.22.5 vs lib/active_support/cache.rb in activesupport-4.0.0.beta1

- old
+ new

@@ -1,11 +1,10 @@ require 'benchmark' require 'zlib' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/benchmark' -require 'active_support/core_ext/exception' require 'active_support/core_ext/class/attribute_accessors' require 'active_support/core_ext/numeric/bytes' require 'active_support/core_ext/numeric/time' require 'active_support/core_ext/object/to_param' require 'active_support/core_ext/string/inflections' @@ -43,12 +42,12 @@ # # => returns a new ActiveSupport::Cache::MemCacheStore object # # Any additional arguments will be passed to the corresponding cache store # class's constructor: # - # ActiveSupport::Cache.lookup_store(:file_store, "/tmp/cache") - # # => same as: ActiveSupport::Cache::FileStore.new("/tmp/cache") + # ActiveSupport::Cache.lookup_store(:file_store, '/tmp/cache') + # # => same as: ActiveSupport::Cache::FileStore.new('/tmp/cache') # # If the first argument is not a Symbol, then it will simply be returned: # # ActiveSupport::Cache.lookup_store(MyOwnCacheStore.new) # # => returns MyOwnCacheStore.new @@ -89,10 +88,11 @@ def retrieve_cache_key(key) case when key.respond_to?(:cache_key) then key.cache_key when key.is_a?(Array) then key.map { |element| retrieve_cache_key(element) }.to_param + when key.respond_to?(:to_a) then retrieve_cache_key(key.to_a) else key.to_param end.to_s end end @@ -107,50 +107,50 @@ # # ActiveSupport::Cache::Store can store any serializable Ruby object. # # cache = ActiveSupport::Cache::MemoryStore.new # - # cache.read("city") # => nil - # cache.write("city", "Duckburgh") - # cache.read("city") # => "Duckburgh" + # cache.read('city') # => nil + # cache.write('city', "Duckburgh") + # cache.read('city') # => "Duckburgh" # # Keys are always translated into Strings and are case sensitive. When an # object is specified as a key and has a +cache_key+ method defined, this # method will be called to define the key. Otherwise, the +to_param+ # method will be called. Hashes and Arrays can also be used as keys. The # elements will be delimited by slashes, and the elements within a Hash # will be sorted by key so they are consistent. # - # cache.read("city") == cache.read(:city) # => true + # cache.read('city') == cache.read(:city) # => true # # Nil values can be cached. # # If your cache is on a shared infrastructure, you can define a namespace # for your cache entries. If a namespace is defined, it will be prefixed on # to every key. The namespace can be either a static value or a Proc. If it # is a Proc, it will be invoked when each key is evaluated so that you can # use application logic to invalidate keys. # - # cache.namespace = lambda { @last_mod_time } # Set the namespace to a variable + # cache.namespace = -> { @last_mod_time } # Set the namespace to a variable # @last_mod_time = Time.now # Invalidate the entire cache by changing namespace # - # # Caches can also store values in a compressed format to save space and # reduce time spent sending data. Since there is overhead, values must be # large enough to warrant compression. To turn on compression either pass - # <tt>:compress => true</tt> in the initializer or as an option to +fetch+ + # <tt>compress: true</tt> in the initializer or as an option to +fetch+ # or +write+. To specify the threshold at which to compress values, set the # <tt>:compress_threshold</tt> option. The default threshold is 16K. class Store cattr_accessor :logger, :instance_writer => true attr_reader :silence, :options alias :silence? :silence - # Create a new cache. The options will be passed to any write method calls except - # for :namespace which can be used to set the global namespace for the cache. + # Create a new cache. The options will be passed to any write method calls + # except for <tt>:namespace</tt> which can be used to set the global + # namespace for the cache. def initialize(options = nil) @options = options ? options.dup : {} end # Silence the logger. @@ -165,11 +165,12 @@ yield ensure @silence = previous_silence end - # Set to true if cache stores should be instrumented. Default is false. + # Set to +true+ if cache stores should be instrumented. + # Default is +false+. def self.instrument=(boolean) Thread.current[:instrument_cache_store] = boolean end def self.instrument @@ -177,138 +178,122 @@ end # Fetches data from the cache, using the given key. If there is data in # the cache with the given key, then that data is returned. # - # If there is no such data in the cache (a cache miss), then nil will be - # returned. However, if a block has been passed, that block will be run - # in the event of a cache miss. The return value of the block will be - # written to the cache under the given cache key, and that return value - # will be returned. + # If there is no such data in the cache (a cache miss), then +nil+ will be + # returned. However, if a block has been passed, that block will be passed + # the key and executed in the event of a cache miss. The return value of the + # block will be written to the cache under the given cache key, and that + # return value will be returned. # - # cache.write("today", "Monday") - # cache.fetch("today") # => "Monday" + # cache.write('today', 'Monday') + # cache.fetch('today') # => "Monday" # - # cache.fetch("city") # => nil - # cache.fetch("city") do - # "Duckburgh" + # cache.fetch('city') # => nil + # cache.fetch('city') do + # 'Duckburgh' # end - # cache.fetch("city") # => "Duckburgh" + # cache.fetch('city') # => "Duckburgh" # # You may also specify additional options via the +options+ argument. - # Setting <tt>:force => true</tt> will force a cache miss: + # Setting <tt>force: true</tt> will force a cache miss: # - # cache.write("today", "Monday") - # cache.fetch("today", :force => true) # => nil + # cache.write('today', 'Monday') + # cache.fetch('today', force: true) # => nil # # Setting <tt>:compress</tt> will store a large cache entry set by the call # in a compressed format. # - # # Setting <tt>:expires_in</tt> will set an expiration time on the cache. # All caches support auto-expiring content after a specified number of # seconds. This value can be specified as an option to the constructor # (in which case all entries will be affected), or it can be supplied to # the +fetch+ or +write+ method to effect just one entry. # - # cache = ActiveSupport::Cache::MemoryStore.new(:expires_in => 5.minutes) - # cache.write(key, value, :expires_in => 1.minute) # Set a lower value for one entry + # cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 5.minutes) + # cache.write(key, value, expires_in: 1.minute) # Set a lower value for one entry # - # Setting <tt>:race_condition_ttl</tt> is very useful in situations where a cache entry - # is used very frequently and is under heavy load. If a cache expires and due to heavy load - # seven different processes will try to read data natively and then they all will try to - # write to cache. To avoid that case the first process to find an expired cache entry will - # bump the cache expiration time by the value set in <tt>:race_condition_ttl</tt>. Yes - # this process is extending the time for a stale value by another few seconds. Because - # of extended life of the previous cache, other processes will continue to use slightly - # stale data for a just a big longer. In the meantime that first process will go ahead - # and will write into cache the new value. After that all the processes will start - # getting new value. The key is to keep <tt>:race_condition_ttl</tt> small. + # Setting <tt>:race_condition_ttl</tt> is very useful in situations where + # a cache entry is used very frequently and is under heavy load. If a + # cache expires and due to heavy load seven different processes will try + # to read data natively and then they all will try to write to cache. To + # avoid that case the first process to find an expired cache entry will + # bump the cache expiration time by the value set in <tt>:race_condition_ttl</tt>. + # Yes, this process is extending the time for a stale value by another few + # seconds. Because of extended life of the previous cache, other processes + # will continue to use slightly stale data for a just a big longer. In the + # meantime that first process will go ahead and will write into cache the + # new value. After that all the processes will start getting new value. + # The key is to keep <tt>:race_condition_ttl</tt> small. # - # If the process regenerating the entry errors out, the entry will be regenerated - # after the specified number of seconds. Also note that the life of stale cache is - # extended only if it expired recently. Otherwise a new value is generated and - # <tt>:race_condition_ttl</tt> does not play any role. + # If the process regenerating the entry errors out, the entry will be + # regenerated after the specified number of seconds. Also note that the + # life of stale cache is extended only if it expired recently. Otherwise + # a new value is generated and <tt>:race_condition_ttl</tt> does not play + # any role. # # # Set all values to expire after one minute. - # cache = ActiveSupport::Cache::MemoryStore.new(:expires_in => 1.minute) + # cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 1.minute) # - # cache.write("foo", "original value") + # cache.write('foo', 'original value') # val_1 = nil # val_2 = nil # sleep 60 # # Thread.new do - # val_1 = cache.fetch("foo", :race_condition_ttl => 10) do + # val_1 = cache.fetch('foo', race_condition_ttl: 10) do # sleep 1 - # "new value 1" + # 'new value 1' # end # end # # Thread.new do - # val_2 = cache.fetch("foo", :race_condition_ttl => 10) do - # "new value 2" + # val_2 = cache.fetch('foo', race_condition_ttl: 10) do + # 'new value 2' # end # end # # # val_1 => "new value 1" # # val_2 => "original value" # # sleep 10 # First thread extend the life of cache by another 10 seconds - # # cache.fetch("foo") => "new value 1" + # # cache.fetch('foo') => "new value 1" # # Other options will be handled by the specific cache store implementation. - # Internally, #fetch calls #read_entry, and calls #write_entry on a cache miss. - # +options+ will be passed to the #read and #write calls. + # Internally, #fetch calls #read_entry, and calls #write_entry on a cache + # miss. +options+ will be passed to the #read and #write calls. # # For example, MemCacheStore's #write method supports the +:raw+ # option, which tells the memcached server to store all values as strings. # We can use this option with #fetch too: # # cache = ActiveSupport::Cache::MemCacheStore.new - # cache.fetch("foo", :force => true, :raw => true) do + # cache.fetch("foo", force: true, raw: true) do # :bar # end - # cache.fetch("foo") # => "bar" + # cache.fetch('foo') # => "bar" def fetch(name, options = nil) if block_given? options = merged_options(options) key = namespaced_key(name, options) - unless options[:force] - entry = instrument(:read, name, options) do |payload| - payload[:super_operation] = :fetch if payload - read_entry(key, options) - end - end - if entry && entry.expired? - race_ttl = options[:race_condition_ttl].to_f - if race_ttl and Time.now.to_f - entry.expires_at <= race_ttl - entry.expires_at = Time.now + race_ttl - write_entry(key, entry, :expires_in => race_ttl * 2) - else - delete_entry(key, options) - end - entry = nil - end + cached_entry = find_cached_entry(key, name, options) unless options[:force] + entry = handle_expired_entry(cached_entry, key, options) + if entry - instrument(:fetch_hit, name, options) { |payload| } - entry.value + get_entry_value(entry, name, options) else - result = instrument(:generate, name, options) do |payload| - yield - end - write(name, result, options) - result + save_block_result_to_cache(name, options) { |_name| yield _name } end else read(name, options) end end # Fetches data from the cache, using the given key. If there is data in # the cache with the given key, then that data is returned. Otherwise, - # nil is returned. + # +nil+ is returned. # # Options are passed to the underlying cache implementation. def read(name, options = nil) options = merged_options(options) key = namespaced_key(name, options) @@ -373,22 +358,18 @@ instrument(:delete, name) do |payload| delete_entry(namespaced_key(name, options), options) end end - # Return true if the cache contains an entry for the given key. + # Return +true+ if the cache contains an entry for the given key. # # Options are passed to the underlying cache implementation. def exist?(name, options = nil) options = merged_options(options) instrument(:exist?, name) do |payload| entry = read_entry(namespaced_key(name, options), options) - if entry && !entry.expired? - true - else - false - end + entry && !entry.expired? end end # Delete all entries with keys matching the pattern. # @@ -406,11 +387,11 @@ # All implementations may not support this method. def increment(name, amount = 1, options = nil) raise NotImplementedError.new("#{self.class.name} does not support increment") end - # Increment an integer value in the cache. + # Decrement an integer value in the cache. # # Options are passed to the underlying cache implementation. # # All implementations may not support this method. def decrement(name, amount = 1, options = nil) @@ -435,13 +416,14 @@ def clear(options = nil) raise NotImplementedError.new("#{self.class.name} does not support clear") end protected - # Add the namespace defined in the options to a pattern designed to match keys. - # Implementations that support delete_matched should call this method to translate - # a pattern that matches names into one that matches namespaced keys. + # Add the namespace defined in the options to a pattern designed to + # match keys. Implementations that support delete_matched should call + # this method to translate a pattern that matches names into one that + # matches namespaced keys. def key_matcher(pattern, options) prefix = options[:namespace].is_a?(Proc) ? options[:namespace].call : options[:namespace] if prefix source = pattern.source if source.start_with?('^') @@ -453,21 +435,24 @@ else pattern end end - # Read an entry from the cache implementation. Subclasses must implement this method. + # Read an entry from the cache implementation. Subclasses must implement + # this method. def read_entry(key, options) # :nodoc: raise NotImplementedError.new end - # Write an entry to the cache implementation. Subclasses must implement this method. + # Write an entry to the cache implementation. Subclasses must implement + # this method. def write_entry(key, entry, options) # :nodoc: raise NotImplementedError.new end - # Delete an entry from the cache implementation. Subclasses must implement this method. + # Delete an entry from the cache implementation. Subclasses must + # implement this method. def delete_entry(key, options) # :nodoc: raise NotImplementedError.new end private @@ -479,11 +464,11 @@ options.dup end end # Expand key to be a consistent string value. Invoke +cache_key+ if - # object responds to +cache_key+. Otherwise, to_param method will be + # object responds to +cache_key+. Otherwise, +to_param+ method will be # called. If the key is a Hash, then keys will be sorted alphabetically. def expanded_key(key) # :nodoc: return key.cache_key.to_s if key.respond_to?(:cache_key) case key @@ -498,11 +483,12 @@ end key.to_param end - # Prefix a key with the namespace. Namespace and key will be delimited with a colon. + # Prefix a key with the namespace. Namespace and key will be delimited + # with a colon. def namespaced_key(key, options) key = expanded_key(key) namespace = options[:namespace] if options prefix = namespace.is_a?(Proc) ? namespace.call : namespace key = "#{prefix}:#{key}" if prefix @@ -523,116 +509,162 @@ def log(operation, key, options = nil) return unless logger && logger.debug? && !silence? logger.debug("Cache #{operation}: #{key}#{options.blank? ? "" : " (#{options.inspect})"}") end - end - # Entry that is put into caches. It supports expiration time on entries and can compress values - # to save space in the cache. - class Entry - attr_reader :created_at, :expires_in + def find_cached_entry(key, name, options) + instrument(:read, name, options) do |payload| + payload[:super_operation] = :fetch if payload + read_entry(key, options) + end + end - DEFAULT_COMPRESS_LIMIT = 16.kilobytes - - class << self - # Create an entry with internal attributes set. This method is intended to be - # used by implementations that store cache entries in a native format instead - # of as serialized Ruby objects. - def create(raw_value, created_at, options = {}) - entry = new(nil) - entry.instance_variable_set(:@value, raw_value) - entry.instance_variable_set(:@created_at, created_at.to_f) - entry.instance_variable_set(:@compressed, options[:compressed]) - entry.instance_variable_set(:@expires_in, options[:expires_in]) + def handle_expired_entry(entry, key, options) + if entry && entry.expired? + race_ttl = options[:race_condition_ttl].to_i + if race_ttl && (Time.now - entry.expires_at <= race_ttl) + # When an entry has :race_condition_ttl defined, put the stale entry back into the cache + # for a brief period while the entry is begin recalculated. + entry.expires_at = Time.now + race_ttl + write_entry(key, entry, :expires_in => race_ttl * 2) + else + delete_entry(key, options) + end + entry = nil + end entry end - end + def get_entry_value(entry, name, options) + instrument(:fetch_hit, name, options) { |payload| } + entry.value + end + + def save_block_result_to_cache(name, options) + result = instrument(:generate, name, options) do |payload| + yield(name) + end + write(name, result, options) + result + end + end + + # This class is used to represent cache entries. Cache entries have a value and an optional + # expiration time. The expiration time is used to support the :race_condition_ttl option + # on the cache. + # + # Since cache entries in most instances will be serialized, the internals of this class are highly optimized + # using short instance variable names that are lazily defined. + class Entry # :nodoc: + DEFAULT_COMPRESS_LIMIT = 16.kilobytes + # Create a new cache entry for the specified value. Options supported are # +:compress+, +:compress_threshold+, and +:expires_in+. def initialize(value, options = {}) - @compressed = false - @expires_in = options[:expires_in] - @expires_in = @expires_in.to_f if @expires_in - @created_at = Time.now.to_f - if value.nil? - @value = nil + if should_compress?(value, options) + @v = compress(value) + @c = true else - @value = Marshal.dump(value) - if should_compress?(@value, options) - @value = Zlib::Deflate.deflate(@value) - @compressed = true - end + @v = value end + if expires_in = options[:expires_in] + @x = (Time.now + expires_in).to_i + end end - # Get the raw value. This value may be serialized and compressed. - def raw_value - @value - end - - # Get the value stored in the cache. def value - # If the original value was exactly false @value is still true because - # it is marshalled and eventually compressed. Both operations yield - # strings. - if @value - # In rails 3.1 and earlier values in entries did not marshaled without - # options[:compress] and if it's Numeric. - # But after commit a263f377978fc07515b42808ebc1f7894fafaa3a - # all values in entries are marshalled. And after that code below expects - # that all values in entries will be marshaled (and will be strings). - # So here we need a check for old ones. - begin - Marshal.load(compressed? ? Zlib::Inflate.inflate(@value) : @value) - rescue TypeError - compressed? ? Zlib::Inflate.inflate(@value) : @value - end - end + convert_version_3_entry! if defined?(@value) + compressed? ? uncompress(@v) : @v end - def compressed? - @compressed - end - - # Check if the entry is expired. The +expires_in+ parameter can override the - # value set when the entry was created. + # Check if the entry is expired. The +expires_in+ parameter can override + # the value set when the entry was created. def expired? - @expires_in && @created_at + @expires_in <= Time.now.to_f - end - - # Set a new time when the entry will expire. - def expires_at=(time) - if time - @expires_in = time.to_f - @created_at + convert_version_3_entry! if defined?(@value) + if defined?(@x) + @x && @x < Time.now.to_i else - @expires_in = nil + false end end - # Seconds since the epoch when the entry will expire. def expires_at - @expires_in ? @created_at + @expires_in : nil + Time.at(@x) if defined?(@x) end - # Returns the size of the cached value. This could be less than value.size - # if the data is compressed. + def expires_at=(value) + @x = value.to_i + end + + # Returns the size of the cached value. This could be less than + # <tt>value.size</tt> if the data is compressed. def size - if @value.nil? - 0 + if defined?(@s) + @s else - @value.bytesize + case value + when NilClass + 0 + when String + @v.bytesize + else + @s = Marshal.dump(@v).bytesize + end end end + # Duplicate the value in a class. This is used by cache implementations that don't natively + # serialize entries to protect against accidental cache modifications. + def dup_value! + convert_version_3_entry! if defined?(@value) + if @v && !compressed? && !(@v.is_a?(Numeric) || @v == true || @v == false) + if @v.is_a?(String) + @v = @v.dup + else + @v = Marshal.load(Marshal.dump(@v)) + end + end + end + private - def should_compress?(serialized_value, options) - if options[:compress] + def should_compress?(value, options) + if value && options[:compress] compress_threshold = options[:compress_threshold] || DEFAULT_COMPRESS_LIMIT - return true if serialized_value.size >= compress_threshold + serialized_value_size = (value.is_a?(String) ? value : Marshal.dump(value)).bytesize + return true if serialized_value_size >= compress_threshold end false + end + + def compressed? + defined?(@c) ? @c : false + end + + def compress(value) + Zlib::Deflate.deflate(Marshal.dump(value)) + end + + def uncompress(value) + Marshal.load(Zlib::Inflate.inflate(value)) + end + + # The internals of this method changed between Rails 3.x and 4.0. This method provides the glue + # to ensure that cache entries created under the old version still work with the new class definition. + def convert_version_3_entry! + if defined?(@value) + @v = @value + remove_instance_variable(:@value) + end + if defined?(@compressed) + @c = @compressed + remove_instance_variable(:@compressed) + end + if defined?(@expires_in) && defined?(@created_at) && @expires_in && @created_at + @x = (@created_at + @expires_in).to_i + remove_instance_variable(:@created_at) + remove_instance_variable(:@expires_in) + end end end end end