lib/active_support/cache.rb in activesupport-6.0.6.1 vs lib/active_support/cache.rb in activesupport-6.1.0.rc1

- old
+ new

@@ -1,14 +1,16 @@ # frozen_string_literal: true require "zlib" require "active_support/core_ext/array/extract_options" require "active_support/core_ext/array/wrap" +require "active_support/core_ext/enumerable" require "active_support/core_ext/module/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/object/try" require "active_support/core_ext/string/inflections" module ActiveSupport # See ActiveSupport::Cache::Store for documentation. module Cache @@ -18,11 +20,11 @@ autoload :NullStore, "active_support/cache/null_store" autoload :RedisCacheStore, "active_support/cache/redis_cache_store" # These options mean something to all cache implementations. Individual cache # implementations may support additional options. - UNIVERSAL_OPTIONS = [:namespace, :compress, :compress_threshold, :expires_in, :race_condition_ttl] + UNIVERSAL_OPTIONS = [:namespace, :compress, :compress_threshold, :expires_in, :race_condition_ttl, :coder] module Strategy autoload :LocalCache, "active_support/cache/strategy/local_cache" end @@ -77,11 +79,11 @@ # ActiveSupport::Cache.expand_cache_key([:foo, :bar]) # => "foo/bar" # ActiveSupport::Cache.expand_cache_key([:foo, :bar], "namespace") # => "namespace/foo/bar" # # The +key+ argument can also respond to +cache_key+ or +to_param+. def expand_cache_key(key, namespace = nil) - expanded_cache_key = (namespace ? "#{namespace}/" : "").dup + expanded_cache_key = namespace ? +"#{namespace}/" : +"" if prefix = ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"] expanded_cache_key << "#{prefix}/" end @@ -154,10 +156,12 @@ # compression, pass <tt>compress: false</tt> to the initializer or to # individual +fetch+ or +write+ method calls. The 1kB compression # threshold is configurable with the <tt>:compress_threshold</tt> option, # specified in bytes. class Store + DEFAULT_CODER = Marshal + cattr_accessor :logger, instance_writer: true attr_reader :silence, :options alias :silence? :silence @@ -181,10 +185,11 @@ # Creates 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 : {} + @coder = @options.delete(:coder) { self.class::DEFAULT_CODER } || NullCoder end # Silences the logger. def silence! @silence = true @@ -439,12 +444,12 @@ options = merged_options(options) instrument :read_multi, names, options do |payload| reads = read_multi_entries(names, **options) writes = {} - ordered = names.each_with_object({}) do |name, hash| - hash[name] = reads.fetch(name) { writes[name] = yield(name) } + ordered = names.index_with do |name| + reads.fetch(name) { writes[name] = yield(name) } end payload[:hits] = reads.keys payload[:super_operation] = :fetch_multi @@ -475,10 +480,22 @@ instrument(:delete, name) do delete_entry(normalize_key(name, options), **options) end end + # Deletes multiple entries in the cache. + # + # Options are passed to the underlying cache implementation. + def delete_multi(names, options = nil) + options = merged_options(options) + names.map! { |key| normalize_key(key, options) } + + instrument :delete_multi, names do + delete_multi_entries(names, **options) + end + end + # Returns +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) @@ -565,30 +582,35 @@ # this method. def write_entry(key, entry, **options) raise NotImplementedError.new end + def serialize_entry(entry) + @coder.dump(entry) + end + + def deserialize_entry(payload) + payload.nil? ? nil : @coder.load(payload) + end + # Reads multiple entries from the cache implementation. Subclasses MAY # implement this method. def read_multi_entries(names, **options) - results = {} - names.each do |name| - key = normalize_key(name, options) + names.each_with_object({}) do |name, results| + key = normalize_key(name, options) + entry = read_entry(key, **options) + + next unless entry + version = normalize_version(name, options) - entry = read_entry(key, **options) - if entry - if entry.expired? - delete_entry(key, **options) - elsif entry.mismatched?(version) - # Skip mismatched versions - else - results[name] = entry.value - end + if entry.expired? + delete_entry(key, **options) + elsif !entry.mismatched?(version) + results[name] = entry.value end end - results end # Writes multiple entries to the cache implementation. Subclasses MAY # implement this method. def write_multi_entries(hash, **options) @@ -601,10 +623,16 @@ # implement this method. def delete_entry(key, **options) raise NotImplementedError.new end + # Deletes multiples entries in the cache implementation. Subclasses MAY + # implement this method. + def delete_multi_entries(entries, **options) + entries.count { |key| delete_entry(key, **options) } + end + # Merges the default options with ones specific to a method call. def merged_options(call_options) if call_options if options.empty? call_options @@ -637,10 +665,14 @@ if namespace.respond_to?(:call) namespace = namespace.call end + if key && key.encoding != Encoding::UTF_8 + key = key.dup.force_encoding(Encoding::UTF_8) + end + if namespace "#{namespace}:#{key}" else key end @@ -653,46 +685,43 @@ return key.cache_key.to_s if key.respond_to?(:cache_key) case key when Array if key.size > 1 - key = key.collect { |element| expanded_key(element) } + key.collect { |element| expanded_key(element) } else - key = expanded_key(key.first) + expanded_key(key.first) end when Hash - key = key.sort_by { |k, _| k.to_s }.collect { |k, v| "#{k}=#{v}" } - end - - key.to_param + key.collect { |k, v| "#{k}=#{v}" }.sort! + else + key + end.to_param end def normalize_version(key, options = nil) (options && options[:version].try(:to_param)) || expanded_version(key) end def expanded_version(key) case when key.respond_to?(:cache_version) then key.cache_version.to_param - when key.is_a?(Array) then key.map { |element| expanded_version(element) }.compact.to_param + when key.is_a?(Array) then key.map { |element| expanded_version(element) }.tap(&:compact!).to_param when key.respond_to?(:to_a) then expanded_version(key.to_a) end end def instrument(operation, key, options = nil) - log { "Cache #{operation}: #{normalize_key(key, options)}#{options.blank? ? "" : " (#{options.inspect})"}" } + if logger && logger.debug? && !silence? + logger.debug "Cache #{operation}: #{normalize_key(key, options)}#{options.blank? ? "" : " (#{options.inspect})"}" + end - payload = { key: key } + payload = { key: key, store: self.class.name } payload.merge!(options) if options.is_a?(Hash) ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload) { yield(payload) } end - def log - return unless logger && logger.debug? && !silence? - logger.debug(yield) - end - def handle_expired_entry(entry, key, options) if entry && entry.expired? race_ttl = options[:race_condition_ttl].to_i if (race_ttl > 0) && (Time.now.to_f - entry.expires_at <= race_ttl) # When an entry has a positive :race_condition_ttl defined, put the stale entry back into the cache @@ -720,10 +749,22 @@ write(name, result, options) unless result.nil? && options[:skip_nil] result end end + module NullCoder # :nodoc: + class << self + def load(payload) + payload + end + + def dump(entry) + entry + end + end + end + # This class is used to represent cache entries. Cache entries have a value, an optional # expiration time, and an optional version. The expiration time is used to support the :race_condition_ttl option # on the cache. The version is used to support the :version option on the cache for rejecting # mismatches. # @@ -770,11 +811,11 @@ @expires_in = nil end end # Returns the size of the cached value. This could be less than - # <tt>value.size</tt> if the data is compressed. - def size + # <tt>value.bytesize</tt> if the data is compressed. + def bytesize case value when NilClass 0 when String @value.bytesize