mirror of https://github.com/rails/rails
Support :message_pack as a cache serializer format
This commit adds support for `:message_pack` as an option for `config.active_support.cache_format_version`. Cache entries written using the `6.1` or `7.0` formats can be read when using the `:message_pack` format. Additionally, cache entries written using the `:message_pack` format can now be read when using the `6.1` or `7.0` format. These behaviors makes it easy to migrate between formats without invalidating the entire cache.
This commit is contained in:
parent
dae86240a2
commit
e2524e574b
|
@ -1,3 +1,16 @@
|
|||
* `config.active_support.cache_format_version` now accepts `:message_pack` as
|
||||
an option. `:message_pack` can reduce cache entry sizes and improve
|
||||
performance, but requires the [`msgpack` gem](https://rubygems.org/gems/msgpack)
|
||||
(>= 1.7.0).
|
||||
|
||||
Cache entries written using the `6.1` or `7.0` cache formats can be read
|
||||
when using the `:message_pack` cache format. Additionally, cache entries
|
||||
written using the `:message_pack` cache format can now be read when using
|
||||
the `6.1` or `7.0` cache formats. These behaviors makes it easy to migrate
|
||||
between formats without invalidating the entire cache.
|
||||
|
||||
*Jonathan Hefner*
|
||||
|
||||
* `Object#deep_dup` no longer duplicate named classes and modules.
|
||||
|
||||
Before:
|
||||
|
|
|
@ -8,6 +8,7 @@ require "active_support/core_ext/numeric/bytes"
|
|||
require "active_support/core_ext/object/to_param"
|
||||
require "active_support/core_ext/object/try"
|
||||
require "active_support/core_ext/string/inflections"
|
||||
require_relative "cache/serializer_with_fallback"
|
||||
|
||||
module ActiveSupport
|
||||
# See ActiveSupport::Cache::Store for documentation.
|
||||
|
@ -645,7 +646,14 @@ module ActiveSupport
|
|||
|
||||
private
|
||||
def default_coder
|
||||
Coders[Cache.format_version]
|
||||
case Cache.format_version
|
||||
when 6.1
|
||||
Cache::SerializerWithFallback[:marshal_6_1]
|
||||
when 7.0
|
||||
Cache::SerializerWithFallback[:marshal_7_0]
|
||||
else
|
||||
Cache::SerializerWithFallback[Cache.format_version]
|
||||
end
|
||||
end
|
||||
|
||||
# Adds the namespace defined in the options to a pattern designed to
|
||||
|
@ -942,82 +950,6 @@ module ActiveSupport
|
|||
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)
|
||||
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)
|
||||
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
|
||||
|
|
|
@ -222,52 +222,12 @@ module ActiveSupport
|
|||
end
|
||||
|
||||
private
|
||||
module Coders # :nodoc:
|
||||
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
|
||||
def load(payload)
|
||||
if payload.is_a?(Entry)
|
||||
payload
|
||||
else
|
||||
Cache::Coders::Loader.load(payload)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module Rails61Coder
|
||||
include Loader
|
||||
extend self
|
||||
|
||||
def dump(entry)
|
||||
entry
|
||||
end
|
||||
|
||||
def dump_compressed(entry, threshold)
|
||||
entry.compressed(threshold)
|
||||
end
|
||||
end
|
||||
|
||||
module Rails70Coder
|
||||
include Cache::Coders::Rails70Coder
|
||||
include Loader
|
||||
extend self
|
||||
end
|
||||
end
|
||||
|
||||
def default_coder
|
||||
Coders[Cache.format_version]
|
||||
if Cache.format_version == 6.1
|
||||
Cache::SerializerWithFallback[:passthrough]
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
# Read an entry from the cache.
|
||||
|
|
|
@ -0,0 +1,174 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ActiveSupport
|
||||
module Cache
|
||||
module SerializerWithFallback # :nodoc:
|
||||
def self.[](format)
|
||||
if format.to_s.include?("message_pack") && !defined?(ActiveSupport::MessagePack)
|
||||
require "active_support/message_pack"
|
||||
end
|
||||
|
||||
SERIALIZERS.fetch(format)
|
||||
end
|
||||
|
||||
def dump_compressed(entry, threshold)
|
||||
dumped = dump(entry)
|
||||
try_compress(dumped, threshold) || dumped
|
||||
end
|
||||
|
||||
def load(dumped)
|
||||
if dumped.is_a?(String)
|
||||
dumped = decompress(dumped) if compressed?(dumped)
|
||||
|
||||
case
|
||||
when MessagePackWithFallback.dumped?(dumped)
|
||||
MessagePackWithFallback._load(dumped)
|
||||
when Marshal70WithFallback.dumped?(dumped)
|
||||
Marshal70WithFallback._load(dumped)
|
||||
when Marshal61WithFallback.dumped?(dumped)
|
||||
Marshal61WithFallback._load(dumped)
|
||||
else
|
||||
Cache::Store.logger&.warn("Unrecognized payload prefix #{dumped.byteslice(0).inspect}; deserializing as nil")
|
||||
nil
|
||||
end
|
||||
elsif PassthroughWithFallback.dumped?(dumped)
|
||||
PassthroughWithFallback._load(dumped)
|
||||
else
|
||||
Cache::Store.logger&.warn("Unrecognized payload class #{dumped.class}; deserializing as nil")
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
ZLIB_HEADER = "\x78".b.freeze
|
||||
|
||||
def compressed?(dumped)
|
||||
dumped.start_with?(ZLIB_HEADER)
|
||||
end
|
||||
|
||||
def compress(dumped)
|
||||
Zlib::Deflate.deflate(dumped)
|
||||
end
|
||||
|
||||
def try_compress(dumped, threshold)
|
||||
if dumped.bytesize >= threshold
|
||||
compressed = compress(dumped)
|
||||
compressed unless compressed.bytesize >= dumped.bytesize
|
||||
end
|
||||
end
|
||||
|
||||
def decompress(compressed)
|
||||
Zlib::Inflate.inflate(compressed)
|
||||
end
|
||||
|
||||
module PassthroughWithFallback
|
||||
include SerializerWithFallback
|
||||
extend self
|
||||
|
||||
def dump(entry)
|
||||
entry
|
||||
end
|
||||
|
||||
def dump_compressed(entry, threshold)
|
||||
entry.compressed(threshold)
|
||||
end
|
||||
|
||||
def _load(entry)
|
||||
entry
|
||||
end
|
||||
|
||||
def dumped?(dumped)
|
||||
dumped.is_a?(Cache::Entry)
|
||||
end
|
||||
end
|
||||
|
||||
module Marshal61WithFallback
|
||||
include SerializerWithFallback
|
||||
extend self
|
||||
|
||||
MARSHAL_SIGNATURE = "\x04\x08".b.freeze
|
||||
|
||||
def dump(entry)
|
||||
Marshal.dump(entry)
|
||||
end
|
||||
|
||||
def dump_compressed(entry, threshold)
|
||||
Marshal.dump(entry.compressed(threshold))
|
||||
end
|
||||
|
||||
def _load(dumped)
|
||||
Marshal.load(dumped)
|
||||
end
|
||||
|
||||
def dumped?(dumped)
|
||||
dumped.start_with?(MARSHAL_SIGNATURE)
|
||||
end
|
||||
end
|
||||
|
||||
module Marshal70WithFallback
|
||||
include SerializerWithFallback
|
||||
extend self
|
||||
|
||||
MARK_UNCOMPRESSED = "\x00".b.freeze
|
||||
MARK_COMPRESSED = "\x01".b.freeze
|
||||
|
||||
def dump(entry)
|
||||
MARK_UNCOMPRESSED + Marshal.dump(entry.pack)
|
||||
end
|
||||
|
||||
def dump_compressed(entry, threshold)
|
||||
dumped = Marshal.dump(entry.pack)
|
||||
if compressed = try_compress(dumped, threshold)
|
||||
MARK_COMPRESSED + compressed
|
||||
else
|
||||
MARK_UNCOMPRESSED + dumped
|
||||
end
|
||||
end
|
||||
|
||||
def _load(marked)
|
||||
dumped = marked.byteslice(1..-1)
|
||||
dumped = decompress(dumped) if marked.start_with?(MARK_COMPRESSED)
|
||||
Cache::Entry.unpack(Marshal.load(dumped))
|
||||
end
|
||||
|
||||
def dumped?(dumped)
|
||||
dumped.start_with?(MARK_UNCOMPRESSED, MARK_COMPRESSED)
|
||||
end
|
||||
end
|
||||
|
||||
module MessagePackWithFallback
|
||||
include SerializerWithFallback
|
||||
extend self
|
||||
|
||||
def dump(entry)
|
||||
ActiveSupport::MessagePack::CacheSerializer.dump(entry.pack)
|
||||
end
|
||||
|
||||
def _load(dumped)
|
||||
packed = ActiveSupport::MessagePack::CacheSerializer.load(dumped)
|
||||
Cache::Entry.unpack(packed) if packed
|
||||
end
|
||||
|
||||
def dumped?(dumped)
|
||||
available? && ActiveSupport::MessagePack.signature?(dumped)
|
||||
end
|
||||
|
||||
private
|
||||
def available?
|
||||
return @available if defined?(@available)
|
||||
require "active_support/message_pack"
|
||||
@available = true
|
||||
rescue LoadError
|
||||
@available = false
|
||||
end
|
||||
end
|
||||
|
||||
SERIALIZERS = {
|
||||
passthrough: PassthroughWithFallback,
|
||||
marshal_6_1: Marshal61WithFallback,
|
||||
marshal_7_0: Marshal70WithFallback,
|
||||
message_pack: MessagePackWithFallback,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -8,34 +8,13 @@ module ActiveSupport
|
|||
include Serializer
|
||||
extend self
|
||||
|
||||
ZLIB_HEADER = "\x78"
|
||||
|
||||
def dump(entry)
|
||||
super(entry.pack)
|
||||
end
|
||||
|
||||
def dump_compressed(entry, threshold) # :nodoc:
|
||||
dumped = dump(entry)
|
||||
if dumped.bytesize >= threshold
|
||||
compressed = Zlib::Deflate.deflate(dumped)
|
||||
compressed.bytesize < dumped.bytesize ? compressed : dumped
|
||||
else
|
||||
dumped
|
||||
end
|
||||
end
|
||||
|
||||
def load(dumped)
|
||||
dumped = Zlib::Inflate.inflate(dumped) if compressed?(dumped)
|
||||
ActiveSupport::Cache::Entry.unpack(super)
|
||||
super
|
||||
rescue ActiveSupport::MessagePack::MissingClassError
|
||||
# Treat missing class as cache miss => return nil
|
||||
end
|
||||
|
||||
private
|
||||
def compressed?(dumped)
|
||||
dumped.start_with?(ZLIB_HEADER)
|
||||
end
|
||||
|
||||
def install_unregistered_type_handler
|
||||
Extensions.install_unregistered_type_fallback(message_pack_factory)
|
||||
end
|
||||
|
|
|
@ -6,6 +6,7 @@ require_relative "behaviors/cache_instrumentation_behavior"
|
|||
require_relative "behaviors/cache_store_behavior"
|
||||
require_relative "behaviors/cache_store_version_behavior"
|
||||
require_relative "behaviors/cache_store_coder_behavior"
|
||||
require_relative "behaviors/cache_store_format_version_behavior"
|
||||
require_relative "behaviors/connection_pool_behavior"
|
||||
require_relative "behaviors/encoded_key_cache_behavior"
|
||||
require_relative "behaviors/failure_safety_behavior"
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/core_ext/object/with"
|
||||
|
||||
module CacheStoreFormatVersionBehavior
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
FORMAT_VERSIONS = [6.1, 7.0, :message_pack]
|
||||
|
||||
included do
|
||||
test "format version affects default coder" do
|
||||
coders = FORMAT_VERSIONS.map do |format_version|
|
||||
ActiveSupport::Cache.with(format_version: format_version) do
|
||||
lookup_store.instance_variable_get(:@coder)
|
||||
end
|
||||
end
|
||||
|
||||
assert_equal coders, coders.uniq
|
||||
end
|
||||
|
||||
test "invalid format version raises" do
|
||||
ActiveSupport::Cache.with(format_version: 0) do
|
||||
assert_raises do
|
||||
lookup_store
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
FORMAT_VERSIONS.product(FORMAT_VERSIONS) do |read_version, write_version|
|
||||
test "format version #{read_version.inspect} can read #{write_version.inspect} entries" do
|
||||
key = SecureRandom.uuid
|
||||
|
||||
ActiveSupport::Cache.with(format_version: write_version) do
|
||||
lookup_store.write(key, "value for #{key}")
|
||||
end
|
||||
|
||||
ActiveSupport::Cache.with(format_version: read_version) do
|
||||
assert_equal "value for #{key}", lookup_store.read(key)
|
||||
end
|
||||
end
|
||||
|
||||
test "format version #{read_version.inspect} can read #{write_version.inspect} entries with compression" do
|
||||
key = SecureRandom.uuid
|
||||
|
||||
ActiveSupport::Cache.with(format_version: write_version) do
|
||||
lookup_store(compress_threshold: 1).write(key, key * 10)
|
||||
end
|
||||
|
||||
ActiveSupport::Cache.with(format_version: read_version) do
|
||||
assert_equal key * 10, lookup_store.read(key)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,29 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative "../abstract_unit"
|
||||
require "active_support/cache"
|
||||
require "active_support/core_ext/numeric/time"
|
||||
|
||||
class CacheCoderTest < ActiveSupport::TestCase
|
||||
def test_new_coder_can_read_legacy_payloads
|
||||
entry = ActiveSupport::Cache::Entry.new("foobar", expires_in: 1.hour, version: "v42")
|
||||
deserialized_entry = ActiveSupport::Cache::Coders::Rails70Coder.load(
|
||||
ActiveSupport::Cache::Coders::Rails61Coder.dump(entry),
|
||||
)
|
||||
|
||||
assert_equal entry.value, deserialized_entry.value
|
||||
assert_equal entry.version, deserialized_entry.version
|
||||
assert_equal entry.expires_at, deserialized_entry.expires_at
|
||||
end
|
||||
|
||||
def test_legacy_coder_can_read_new_payloads
|
||||
entry = ActiveSupport::Cache::Entry.new("foobar", expires_in: 1.hour, version: "v42")
|
||||
deserialized_entry = ActiveSupport::Cache::Coders::Rails61Coder.load(
|
||||
ActiveSupport::Cache::Coders::Rails70Coder.dump(entry),
|
||||
)
|
||||
|
||||
assert_equal entry.value, deserialized_entry.value
|
||||
assert_equal entry.version, deserialized_entry.version
|
||||
assert_equal entry.expires_at, deserialized_entry.expires_at
|
||||
end
|
||||
end
|
|
@ -0,0 +1,82 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative "../abstract_unit"
|
||||
require "active_support/core_ext/object/with"
|
||||
|
||||
class CacheSerializerWithFallbackTest < ActiveSupport::TestCase
|
||||
FORMATS = ActiveSupport::Cache::SerializerWithFallback::SERIALIZERS.keys
|
||||
|
||||
setup do
|
||||
@entry = ActiveSupport::Cache::Entry.new(
|
||||
[{ a_boolean: false, a_number: 123, a_string: "x" * 40 }], expires_in: 100, version: "v42"
|
||||
)
|
||||
end
|
||||
|
||||
FORMATS.product(FORMATS) do |load_format, dump_format|
|
||||
test "#{load_format.inspect} serializer can load #{dump_format.inspect} dump" do
|
||||
dumped = serializer(dump_format).dump(@entry)
|
||||
assert_entry @entry, serializer(load_format).load(dumped)
|
||||
end
|
||||
|
||||
test "#{load_format.inspect} serializer can load #{dump_format.inspect} dump with compression" do
|
||||
compressed = serializer(dump_format).dump_compressed(@entry, 1)
|
||||
assert_entry @entry, serializer(load_format).load(compressed)
|
||||
|
||||
uncompressed = serializer(dump_format).dump_compressed(@entry, 100_000)
|
||||
assert_entry @entry, serializer(load_format).load(uncompressed)
|
||||
end
|
||||
end
|
||||
|
||||
FORMATS.each do |format|
|
||||
test "#{format.inspect} serializer can compress entries" do
|
||||
compressed = serializer(format).dump_compressed(@entry, 1)
|
||||
uncompressed = serializer(format).dump_compressed(@entry, 100_000)
|
||||
assert_operator compressed.bytesize, :<, uncompressed.bytesize
|
||||
end
|
||||
|
||||
test "#{format.inspect} serializer handles unrecognized payloads gracefully" do
|
||||
assert_nil serializer(format).load(Object.new)
|
||||
assert_nil serializer(format).load("")
|
||||
end
|
||||
|
||||
test "#{format.inspect} serializer logs unrecognized payloads" do
|
||||
assert_logs(/unrecognized/i) { serializer(format).load(Object.new) }
|
||||
assert_logs(/unrecognized/i) { serializer(format).load("") }
|
||||
end
|
||||
end
|
||||
|
||||
test ":message_pack serializer handles missing class gracefully" do
|
||||
klass = Class.new do
|
||||
def self.name; "DoesNotActuallyExist"; end
|
||||
def self.from_msgpack_ext(string); self.new; end
|
||||
def to_msgpack_ext; ""; end
|
||||
end
|
||||
|
||||
dumped = serializer(:message_pack).dump(ActiveSupport::Cache::Entry.new(klass.new))
|
||||
assert_not_nil dumped
|
||||
assert_nil serializer(:message_pack).load(dumped)
|
||||
end
|
||||
|
||||
test "raises on invalid format name" do
|
||||
assert_raises KeyError do
|
||||
ActiveSupport::Cache::SerializerWithFallback[:invalid_format]
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def serializer(format)
|
||||
ActiveSupport::Cache::SerializerWithFallback[format]
|
||||
end
|
||||
|
||||
def assert_entry(expected, actual)
|
||||
assert_equal expected.value, actual.value
|
||||
assert_equal expected.version, actual.version
|
||||
assert_equal expected.expires_at, actual.expires_at
|
||||
end
|
||||
|
||||
def assert_logs(pattern, &block)
|
||||
io = StringIO.new
|
||||
ActiveSupport::Cache::Store.with(logger: Logger.new(io), &block)
|
||||
assert_match pattern, io.string
|
||||
end
|
||||
end
|
|
@ -32,6 +32,7 @@ class FileStoreTest < ActiveSupport::TestCase
|
|||
include CacheStoreBehavior
|
||||
include CacheStoreVersionBehavior
|
||||
include CacheStoreCoderBehavior
|
||||
include CacheStoreFormatVersionBehavior
|
||||
include CacheDeleteMatchedBehavior
|
||||
include CacheIncrementDecrementBehavior
|
||||
include CacheInstrumentationBehavior
|
||||
|
@ -143,36 +144,3 @@ class FileStoreTest < ActiveSupport::TestCase
|
|||
assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true)
|
||||
end
|
||||
end
|
||||
|
||||
class OptimizedFileStoreTest < FileStoreTest
|
||||
def setup
|
||||
@previous_format = ActiveSupport::Cache.format_version
|
||||
ActiveSupport::Cache.format_version = 7.0
|
||||
super
|
||||
end
|
||||
|
||||
def test_forward_compatibility
|
||||
previous_format = ActiveSupport::Cache.format_version
|
||||
ActiveSupport::Cache.format_version = 6.1
|
||||
@old_store = lookup_store
|
||||
ActiveSupport::Cache.format_version = previous_format
|
||||
|
||||
@old_store.write("foo", "bar")
|
||||
assert_equal "bar", @cache.read("foo")
|
||||
end
|
||||
|
||||
def test_backward_compatibility
|
||||
previous_format = ActiveSupport::Cache.format_version
|
||||
ActiveSupport::Cache.format_version = 6.1
|
||||
@old_store = lookup_store
|
||||
ActiveSupport::Cache.format_version = previous_format
|
||||
|
||||
@cache.write("foo", "bar")
|
||||
assert_equal "bar", @old_store.read("foo")
|
||||
end
|
||||
|
||||
def teardown
|
||||
super
|
||||
ActiveSupport::Cache.format_version = @previous_format
|
||||
end
|
||||
end
|
||||
|
|
|
@ -77,6 +77,7 @@ class MemCacheStoreTest < ActiveSupport::TestCase
|
|||
include CacheStoreBehavior
|
||||
include CacheStoreVersionBehavior
|
||||
include CacheStoreCoderBehavior
|
||||
include CacheStoreFormatVersionBehavior
|
||||
include LocalCacheBehavior
|
||||
include CacheIncrementDecrementBehavior
|
||||
include CacheInstrumentationBehavior
|
||||
|
@ -431,36 +432,3 @@ class MemCacheStoreTest < ActiveSupport::TestCase
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
class OptimizedMemCacheStoreTest < MemCacheStoreTest
|
||||
def setup
|
||||
@previous_format = ActiveSupport::Cache.format_version
|
||||
ActiveSupport::Cache.format_version = 7.0
|
||||
super
|
||||
end
|
||||
|
||||
def teardown
|
||||
super
|
||||
ActiveSupport::Cache.format_version = @previous_format
|
||||
end
|
||||
|
||||
def test_forward_compatibility
|
||||
previous_format = ActiveSupport::Cache.format_version
|
||||
ActiveSupport::Cache.format_version = 6.1
|
||||
@old_store = lookup_store
|
||||
ActiveSupport::Cache.format_version = previous_format
|
||||
|
||||
@old_store.write("foo", "bar")
|
||||
assert_equal "bar", @cache.read("foo")
|
||||
end
|
||||
|
||||
def test_backward_compatibility
|
||||
previous_format = ActiveSupport::Cache.format_version
|
||||
ActiveSupport::Cache.format_version = 6.1
|
||||
@old_store = lookup_store
|
||||
ActiveSupport::Cache.format_version = previous_format
|
||||
|
||||
@cache.write("foo", "bar")
|
||||
assert_equal "bar", @old_store.read("foo")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -143,6 +143,7 @@ module ActiveSupport::Cache::RedisCacheStoreTests
|
|||
include CacheStoreBehavior
|
||||
include CacheStoreVersionBehavior
|
||||
include CacheStoreCoderBehavior
|
||||
include CacheStoreFormatVersionBehavior
|
||||
include LocalCacheBehavior
|
||||
include CacheIncrementDecrementBehavior
|
||||
include CacheInstrumentationBehavior
|
||||
|
@ -225,43 +226,6 @@ module ActiveSupport::Cache::RedisCacheStoreTests
|
|||
end
|
||||
end
|
||||
|
||||
class OptimizedRedisCacheStoreCommonBehaviorTest < RedisCacheStoreCommonBehaviorTest
|
||||
def before_setup
|
||||
@previous_format = ActiveSupport::Cache.format_version
|
||||
ActiveSupport::Cache.format_version = 7.0
|
||||
super
|
||||
end
|
||||
|
||||
def test_forward_compatibility
|
||||
previous_format = ActiveSupport::Cache.format_version
|
||||
ActiveSupport::Cache.format_version = 6.1
|
||||
@old_store = lookup_store
|
||||
ActiveSupport::Cache.format_version = previous_format
|
||||
|
||||
key = SecureRandom.uuid
|
||||
value = SecureRandom.alphanumeric
|
||||
@old_store.write(key, value)
|
||||
assert_equal value, @cache.read(key)
|
||||
end
|
||||
|
||||
def test_backward_compatibility
|
||||
previous_format = ActiveSupport::Cache.format_version
|
||||
ActiveSupport::Cache.format_version = 6.1
|
||||
@old_store = lookup_store
|
||||
ActiveSupport::Cache.format_version = previous_format
|
||||
|
||||
key = SecureRandom.uuid
|
||||
value = SecureRandom.alphanumeric
|
||||
@cache.write(key, value)
|
||||
assert_equal value, @old_store.read(key)
|
||||
end
|
||||
|
||||
def after_teardown
|
||||
super
|
||||
ActiveSupport::Cache.format_version = @previous_format
|
||||
end
|
||||
end
|
||||
|
||||
class ConnectionPoolBehaviorTest < StoreTest
|
||||
include ConnectionPoolBehavior
|
||||
|
||||
|
|
|
@ -27,36 +27,13 @@ class MessagePackCacheSerializerTest < ActiveSupport::TestCase
|
|||
end
|
||||
end
|
||||
|
||||
test "integrates with ActiveSupport::Cache" do
|
||||
with_cache do |cache|
|
||||
value = DefinesFromMsgpackExt.new("foo")
|
||||
cache.write("key", value)
|
||||
assert_equal value, cache.read("key")
|
||||
end
|
||||
end
|
||||
|
||||
test "treats missing class as a cache miss" do
|
||||
test "handles missing class gracefully" do
|
||||
klass = Class.new(DefinesFromMsgpackExt)
|
||||
def klass.name; "DoesNotActuallyExist"; end
|
||||
|
||||
with_cache do |cache|
|
||||
value = klass.new("foo")
|
||||
cache.write("key", value)
|
||||
assert_nil cache.read("key")
|
||||
end
|
||||
end
|
||||
|
||||
test "supports compression" do
|
||||
entry = ActiveSupport::Cache::Entry.new(["foo"] * 100)
|
||||
uncompressed = serializer.dump(entry)
|
||||
compressed = serializer.dump_compressed(entry, 1)
|
||||
|
||||
assert_operator compressed.bytesize, :<, uncompressed.bytesize
|
||||
assert_equal serializer.load(uncompressed).value, serializer.load(compressed).value
|
||||
|
||||
with_cache(compress_threshold: 1) do |cache|
|
||||
assert_equal compressed, cache.send(:serialize_entry, entry)
|
||||
end
|
||||
dumped = dump(klass.new("foo"))
|
||||
assert_not_nil dumped
|
||||
assert_nil load(dumped)
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -64,20 +41,6 @@ class MessagePackCacheSerializerTest < ActiveSupport::TestCase
|
|||
ActiveSupport::MessagePack::CacheSerializer
|
||||
end
|
||||
|
||||
def dump(object)
|
||||
super(ActiveSupport::Cache::Entry.new(object))
|
||||
end
|
||||
|
||||
def load(dumped)
|
||||
super.value
|
||||
end
|
||||
|
||||
def with_cache(**options, &block)
|
||||
Dir.mktmpdir do |dir|
|
||||
block.call(ActiveSupport::Cache::FileStore.new(dir, coder: serializer, **options))
|
||||
end
|
||||
end
|
||||
|
||||
class HasValue
|
||||
attr_reader :value
|
||||
|
||||
|
|
|
@ -2265,7 +2265,19 @@ The default value depends on the `config.load_defaults` target version:
|
|||
|
||||
#### `config.active_support.cache_format_version`
|
||||
|
||||
Specifies which version of the cache serializer to use. Possible values are `6.1` and `7.0`.
|
||||
Specifies which serialization format to use for the cache. Possible values are
|
||||
`6.1`, `7.0`, and `:message_pack`.
|
||||
|
||||
The `6.1` and `7.0` formats both use `Marshal`, but the latter uses a more
|
||||
efficient cache entry representation.
|
||||
|
||||
The `:message_pack` format uses `ActiveSupport::MessagePack`, and may further
|
||||
reduce cache entry sizes and improve performance, but requires the
|
||||
[`msgpack` gem](https://rubygems.org/gems/msgpack).
|
||||
|
||||
All formats are backward and forward compatible, meaning cache entries written
|
||||
in one format can be read when using another format. This behavior makes it
|
||||
easy to migrate between formats without invalidating the entire cache.
|
||||
|
||||
The default value depends on the `config.load_defaults` target version:
|
||||
|
||||
|
|
|
@ -4125,50 +4125,42 @@ module ApplicationTests
|
|||
assert_equal :fiber, ActiveSupport::IsolatedExecutionState.isolation_level
|
||||
end
|
||||
|
||||
test "cache_format_version in a new app" do
|
||||
add_to_config <<-RUBY
|
||||
config.cache_store = :null_store
|
||||
RUBY
|
||||
test "ActiveSupport::Cache.format_version is 7.0 by default for new apps" do
|
||||
app "development"
|
||||
|
||||
assert_equal ActiveSupport::Cache::Coders::Rails70Coder, Rails.cache.instance_variable_get(:@coder)
|
||||
assert_equal 7.0, ActiveSupport::Cache.format_version
|
||||
end
|
||||
|
||||
test "cache_format_version with explicit 7.0 defaults" do
|
||||
add_to_config <<-RUBY
|
||||
config.cache_store = :null_store
|
||||
RUBY
|
||||
test "ActiveSupport::Cache.format_version is 6.1 by default for upgraded apps" do
|
||||
remove_from_config '.*config\.load_defaults.*\n'
|
||||
add_to_config 'config.load_defaults "7.0"'
|
||||
|
||||
app "development"
|
||||
|
||||
assert_equal ActiveSupport::Cache::Coders::Rails70Coder, Rails.cache.instance_variable_get(:@coder)
|
||||
assert_equal 6.1, ActiveSupport::Cache.format_version
|
||||
end
|
||||
|
||||
test "cache_format_version with 6.1 defaults" do
|
||||
add_to_config <<-RUBY
|
||||
config.cache_store = :null_store
|
||||
RUBY
|
||||
test "ActiveSupport::Cache.format_version can be configured via config.active_support.cache_format_version" do
|
||||
remove_from_config '.*config\.load_defaults.*\n'
|
||||
add_to_config 'config.load_defaults "6.1"'
|
||||
|
||||
add_to_config "config.active_support.cache_format_version = 7.0"
|
||||
|
||||
app "development"
|
||||
|
||||
assert_equal ActiveSupport::Cache::Coders::Rails61Coder, Rails.cache.instance_variable_get(:@coder)
|
||||
assert_equal 7.0, ActiveSupport::Cache.format_version
|
||||
end
|
||||
|
||||
test "cache_format_version **cannot** be set via new framework defaults" do
|
||||
add_to_config <<-RUBY
|
||||
config.cache_store = :null_store
|
||||
RUBY
|
||||
test "config.active_support.cache_format_version affects Rails.cache when set in an environment file (or earlier)" do
|
||||
remove_from_config '.*config\.load_defaults.*\n'
|
||||
add_to_config 'config.load_defaults "6.1"'
|
||||
app_file "config/initializers/new_framework_defaults_7_0.rb", <<-RUBY
|
||||
|
||||
app_file "config/environments/development.rb", <<~RUBY
|
||||
Rails.application.config.active_support.cache_format_version = 7.0
|
||||
RUBY
|
||||
|
||||
app "development"
|
||||
|
||||
assert_equal ActiveSupport::Cache::Coders::Rails61Coder, Rails.cache.instance_variable_get(:@coder)
|
||||
assert_equal \
|
||||
ActiveSupport::Cache::NullStore.new.instance_variable_get(:@coder),
|
||||
Rails.cache.instance_variable_get(:@coder)
|
||||
end
|
||||
|
||||
test "raise_on_invalid_cache_expiration_time is false with 7.0 defaults" do
|
||||
|
|
|
@ -381,9 +381,8 @@ module ApplicationTests
|
|||
rails %w(db:migrate)
|
||||
|
||||
add_to_config <<~RUBY
|
||||
require "active_support/message_pack"
|
||||
config.cache_store = :file_store, #{app_path("tmp/cache").inspect},
|
||||
{ coder: ActiveSupport::MessagePack::CacheSerializer }
|
||||
config.cache_store = :file_store, #{app_path("tmp/cache").inspect}
|
||||
config.active_support.cache_format_version = :message_pack
|
||||
RUBY
|
||||
|
||||
require "#{app_path}/config/environment"
|
||||
|
|
Loading…
Reference in New Issue