lib/active_support/cache.rb in activesupport-6.1.7.10 vs lib/active_support/cache.rb in activesupport-7.0.0.alpha1
- old
+ new
@@ -20,17 +20,28 @@
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, :coder]
+ UNIVERSAL_OPTIONS = [:namespace, :compress, :compress_threshold, :expires_in, :expire_in, :expired_in, :race_condition_ttl, :coder, :skip_nil]
+ DEFAULT_COMPRESS_LIMIT = 1.kilobyte
+
+ # Mapping of canonical option names to aliases that a store will recognize.
+ OPTION_ALIASES = {
+ expires_in: [:expire_in, :expired_in]
+ }.freeze
+
module Strategy
autoload :LocalCache, "active_support/cache/strategy/local_cache"
end
+ @format_version = 6.1
+
class << self
+ attr_accessor :format_version
+
# Creates a new Store object according to the given options.
#
# If no arguments are passed to this method, then a new
# ActiveSupport::Cache::MemoryStore object will be returned.
#
@@ -162,12 +173,10 @@
# 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
@@ -190,12 +199,16 @@
# 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
+ @options = options ? normalize_options(options) : {}
+ @options[:compress] = true unless @options.key?(:compress)
+ @options[:compress_threshold] = DEFAULT_COMPRESS_LIMIT unless @options.key?(:compress_threshold)
+
+ @coder = @options.delete(:coder) { default_coder } || NullCoder
+ @coder_supports_compression = @coder.respond_to?(:dump_compressed)
end
# Silences the logger.
def silence!
@silence = true
@@ -253,15 +266,25 @@
#
# 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.
+ # the +fetch+ or +write+ method to affect just one entry.
+ # <tt>:expire_in</tt> and <tt>:expired_in</tt> are aliases for
+ # <tt>:expires_in</tt>.
#
# 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>:expires_at</tt> will set an absolute expiration time on the cache.
+ # All caches support auto-expiring content after a specified number of
+ # seconds. This value can only be supplied to the +fetch+ or +write+ method to
+ # affect just one entry.
+ #
+ # cache = ActiveSupport::Cache::MemoryStore.new
+ # cache.write(key, value, expires_at: Time.now.at_end_of_hour)
+ #
# Setting <tt>:version</tt> verifies the cache stored under <tt>name</tt>
# is of the same version. nil is returned on mismatches despite contents.
# This feature is used to support recyclable cache keys.
#
# Setting <tt>:race_condition_ttl</tt> is very useful in situations where
@@ -510,10 +533,14 @@
entry = read_entry(normalize_key(name, options), **options, event: payload)
(entry && !entry.expired? && !entry.mismatched?(normalize_version(name, options))) || false
end
end
+ def new_entry(value, options = nil) # :nodoc:
+ Entry.new(value, **merged_options(options))
+ end
+
# Deletes all entries with keys matching the pattern.
#
# Options are passed to the underlying cache implementation.
#
# Some implementations may not support this method.
@@ -557,10 +584,14 @@
def clear(options = nil)
raise NotImplementedError.new("#{self.class.name} does not support clear")
end
private
+ def default_coder
+ Coders[Cache.format_version]
+ end
+
# Adds 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) # :doc:
@@ -588,12 +619,17 @@
# this method.
def write_entry(key, entry, **options)
raise NotImplementedError.new
end
- def serialize_entry(entry)
- @coder.dump(entry)
+ def serialize_entry(entry, **options)
+ options = merged_options(options)
+ if @coder_supports_compression && options[:compress]
+ @coder.dump_compressed(entry, options[:compress_threshold] || DEFAULT_COMPRESS_LIMIT)
+ else
+ @coder.dump(entry)
+ end
end
def deserialize_entry(payload)
payload.nil? ? nil : @coder.load(payload)
end
@@ -638,20 +674,33 @@
end
# Merges the default options with ones specific to a method call.
def merged_options(call_options)
if call_options
+ call_options = normalize_options(call_options)
if options.empty?
call_options
else
options.merge(call_options)
end
else
options
end
end
+ # Normalize aliased options to their canonical form
+ def normalize_options(options)
+ options = options.dup
+ OPTION_ALIASES.each do |canonical_name, aliases|
+ alias_key = aliases.detect { |key| options.key?(key) }
+ options[canonical_name] ||= options[alias_key] if alias_key
+ options.except!(*aliases)
+ end
+
+ options
+ end
+
# Expands and namespaces the cache key. May be overridden by
# cache stores to do additional normalization.
def normalize_key(key, options = nil)
namespace_key expanded_key(key), options
end
@@ -730,11 +779,11 @@
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
# for a brief period while the entry is being recalculated.
- entry.expires_at = Time.now + race_ttl
+ entry.expires_at = Time.now.to_f + race_ttl
write_entry(key, entry, expires_in: race_ttl * 2)
else
delete_entry(key, **options)
end
entry = nil
@@ -756,42 +805,125 @@
result
end
end
module NullCoder # :nodoc:
+ extend self
+
+ def dump(entry)
+ entry
+ end
+
+ def dump_compressed(entry, threshold)
+ entry.compressed(threshold)
+ end
+
+ def load(payload)
+ payload
+ end
+ end
+
+ module Coders # :nodoc:
+ MARK_61 = "\x04\b".b.freeze # The one set by Marshal.
+ MARK_70_UNCOMPRESSED = "\x00".b.freeze
+ MARK_70_COMPRESSED = "\x01".b.freeze
+
class << self
+ def [](version)
+ case version
+ when 6.1
+ Rails61Coder
+ when 7.0
+ Rails70Coder
+ else
+ raise ArgumentError, "Unknown ActiveSupport::Cache.format_version #{Cache.format_version.inspect}"
+ end
+ end
+ end
+
+ module Loader
+ extend self
+
def load(payload)
- payload
+ if !payload.is_a?(String)
+ ActiveSupport::Cache::Store.logger&.warn %{Payload wasn't a string, was #{payload.class.name} - couldn't unmarshal, so returning nil."}
+
+ return nil
+ elsif payload.start_with?(MARK_70_UNCOMPRESSED)
+ members = Marshal.load(payload.byteslice(1..-1))
+ elsif payload.start_with?(MARK_70_COMPRESSED)
+ members = Marshal.load(Zlib::Inflate.inflate(payload.byteslice(1..-1)))
+ elsif payload.start_with?(MARK_61)
+ return Marshal.load(payload)
+ else
+ ActiveSupport::Cache::Store.logger&.warn %{Invalid cache prefix: #{payload.byteslice(0).inspect}, expected "\\x00" or "\\x01"}
+
+ return nil
+ end
+ Entry.unpack(members)
end
+ end
+ module Rails61Coder
+ include Loader
+ extend self
+
def dump(entry)
- entry
+ Marshal.dump(entry)
end
+
+ def dump_compressed(entry, threshold)
+ Marshal.dump(entry.compressed(threshold))
+ end
end
+
+ module Rails70Coder
+ include Loader
+ extend self
+
+ def dump(entry)
+ MARK_70_UNCOMPRESSED + Marshal.dump(entry.pack)
+ end
+
+ def dump_compressed(entry, threshold)
+ payload = Marshal.dump(entry.pack)
+ if payload.bytesize >= threshold
+ compressed_payload = Zlib::Deflate.deflate(payload)
+ if compressed_payload.bytesize < payload.bytesize
+ return MARK_70_COMPRESSED + compressed_payload
+ end
+ end
+
+ MARK_70_UNCOMPRESSED + payload
+ 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.
#
# 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:
+ class << self
+ def unpack(members)
+ new(members[0], expires_at: members[1], version: members[2])
+ end
+ end
+
attr_reader :version
- DEFAULT_COMPRESS_LIMIT = 1.kilobyte
-
# Creates a new cache entry for the specified value. Options supported are
- # +:compress+, +:compress_threshold+, +:version+ and +:expires_in+.
- def initialize(value, compress: true, compress_threshold: DEFAULT_COMPRESS_LIMIT, version: nil, expires_in: nil, **)
+ # +:compressed+, +:version+, +:expires_at+ and +:expires_in+.
+ def initialize(value, compressed: false, version: nil, expires_in: nil, expires_at: nil, **)
@value = value
@version = version
- @created_at = Time.now.to_f
- @expires_in = expires_in && expires_in.to_f
-
- compress!(compress_threshold) if compress
+ @created_at = 0.0
+ @expires_in = expires_at&.to_f || expires_in && (expires_in.to_f + Time.now.to_f)
+ @compressed = true if compressed
end
def value
compressed? ? uncompress(@value) : @value
end
@@ -829,10 +961,42 @@
else
@s ||= Marshal.dump(@value).bytesize
end
end
+ def compressed? # :nodoc:
+ defined?(@compressed)
+ end
+
+ def compressed(compress_threshold)
+ return self if compressed?
+
+ case @value
+ when nil, true, false, Numeric
+ uncompressed_size = 0
+ when String
+ uncompressed_size = @value.bytesize
+ else
+ serialized = Marshal.dump(@value)
+ uncompressed_size = serialized.bytesize
+ end
+
+ if uncompressed_size >= compress_threshold
+ serialized ||= Marshal.dump(@value)
+ compressed = Zlib::Deflate.deflate(serialized)
+
+ if compressed.bytesize < uncompressed_size
+ return Entry.new(compressed, compressed: true, expires_at: expires_at, version: version)
+ end
+ end
+ self
+ end
+
+ def local?
+ false
+ end
+
# Duplicates 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!
if @value && !compressed? && !(@value.is_a?(Numeric) || @value == true || @value == false)
if @value.is_a?(String)
@@ -841,36 +1005,16 @@
@value = Marshal.load(Marshal.dump(@value))
end
end
end
- private
- def compress!(compress_threshold)
- case @value
- when nil, true, false, Numeric
- uncompressed_size = 0
- when String
- uncompressed_size = @value.bytesize
- else
- serialized = Marshal.dump(@value)
- uncompressed_size = serialized.bytesize
- end
+ def pack
+ members = [value, expires_at, version]
+ members.pop while !members.empty? && members.last.nil?
+ members
+ end
- if uncompressed_size >= compress_threshold
- serialized ||= Marshal.dump(@value)
- compressed = Zlib::Deflate.deflate(serialized)
-
- if compressed.bytesize < uncompressed_size
- @value = compressed
- @compressed = true
- end
- end
- end
-
- def compressed?
- defined?(@compressed)
- end
-
+ private
def uncompress(value)
Marshal.load(Zlib::Inflate.inflate(value))
end
end
end