From 416a32956cad330f22869e0e0fea01f451a8bfd7 Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Wed, 5 Jul 2023 14:55:26 -0500 Subject: [PATCH 1/8] Format options doc as a description list [ci-skip] --- activesupport/lib/active_support/cache.rb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 7c808517bae66..05fd908a98036 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -231,12 +231,14 @@ def retrieve_pool_options(options) # # ==== Options # - # * +:namespace+ - Sets the namespace for the cache. This option is - # especially useful if your application shares a cache with other - # applications. - # * +:coder+ - Replaces the default cache entry serialization mechanism - # with a custom one. The +coder+ must respond to +dump+ and +load+. - # Using a custom coder disables automatic compression. + # [+:namespace+] + # Sets the namespace for the cache. This option is especially useful if + # your application shares a cache with other applications. + # + # [+:coder+] + # Replaces the default serializer for cache entries. +coder+ must + # respond to +dump+ and +load+. Using a custom coder disables automatic + # compression. # # Alternatively, you can specify coder: :message_pack to use a # preconfigured coder based on ActiveSupport::MessagePack that supports From 860271ee024af458c276340827874b0383af138e Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Wed, 5 Jul 2023 15:26:06 -0500 Subject: [PATCH 2/8] Always set :compress_threshold default value Falsy `:compress_threshold` values are not supported, so we can always set the default value. --- activesupport/lib/active_support/cache.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 05fd908a98036..e8b9c0b71d1e7 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -250,8 +250,9 @@ def retrieve_pool_options(options) # relevant cache operations, such as #read, #write, and #fetch. def initialize(options = nil) @options = options ? normalize_options(options) : {} + @options[:compress] = true unless @options.key?(:compress) - @options[:compress_threshold] = DEFAULT_COMPRESS_LIMIT unless @options.key?(:compress_threshold) + @options[:compress_threshold] ||= DEFAULT_COMPRESS_LIMIT @coder = @options.delete(:coder) { default_coder } || NullCoder @coder = Cache::SerializerWithFallback[@coder] if @coder.is_a?(Symbol) @@ -723,7 +724,7 @@ def write_entry(key, entry, **options) 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) + @coder.dump_compressed(entry, options[:compress_threshold]) else @coder.dump(entry) end From ef703fecac38a59d8ba86aefaf5f71b010fcba27 Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Wed, 5 Jul 2023 15:40:20 -0500 Subject: [PATCH 3/8] Replace NullCoder with :passthrough coder `NullCoder` is functionally equivalent to the `:passthrough` coder. Additionally, `NullCoder` was added in c4845aa7791839fcdf723dc77e3df258e7274496 to serve as the default coder for `MemCacheStore`, but `MemCacheStore` now uses `:passthrough` as its (legacy) default coder. --- activesupport/lib/active_support/cache.rb | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index e8b9c0b71d1e7..318c47657c881 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -254,7 +254,7 @@ def initialize(options = nil) @options[:compress] = true unless @options.key?(:compress) @options[:compress_threshold] ||= DEFAULT_COMPRESS_LIMIT - @coder = @options.delete(:coder) { default_coder } || NullCoder + @coder = @options.delete(:coder) { default_coder } || :passthrough @coder = Cache::SerializerWithFallback[@coder] if @coder.is_a?(Symbol) @coder_supports_compression = @coder.respond_to?(:dump_compressed) end @@ -987,22 +987,6 @@ def expires_at=(expires_at) 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 - # 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 From c7a23018f51f8c90c2b4d10fc8528970671baa1e Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Wed, 5 Jul 2023 15:55:22 -0500 Subject: [PATCH 4/8] Extract Cache::Entry into a separate file `activesupport/lib/active_support/cache.rb` is already rather long, and having a separate file allows subclasses of `Cache::Entry` to be defined without first requiring `activesupport/lib/active_support/cache.rb`. --- activesupport/lib/active_support/cache.rb | 123 +---------------- .../lib/active_support/cache/entry.rb | 128 ++++++++++++++++++ .../cache/serializer_with_fallback.rb | 1 + 3 files changed, 130 insertions(+), 122 deletions(-) create mode 100644 activesupport/lib/active_support/cache/entry.rb diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 318c47657c881..47104db1012e9 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require "zlib" require "active_support/core_ext/array/extract_options" require "active_support/core_ext/enumerable" require "active_support/core_ext/module/attribute_accessors" @@ -8,6 +7,7 @@ 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/entry" require_relative "cache/serializer_with_fallback" module ActiveSupport @@ -986,126 +986,5 @@ def expires_at=(expires_at) @options[:expires_at] = expires_at 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 - - # Creates a new cache entry for the specified value. Options supported are - # +: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 = 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 - - def mismatched?(version) - @version && version && @version != version - end - - # Checks 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 - - def expires_at - @expires_in ? @created_at + @expires_in : nil - end - - def expires_at=(value) - if value - @expires_in = value.to_f - @created_at - else - @expires_in = nil - end - end - - # Returns the size of the cached value. This could be less than - # value.bytesize if the data is compressed. - def bytesize - case value - when NilClass - 0 - when String - @value.bytesize - 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) - @value = @value.dup - else - @value = Marshal.load(Marshal.dump(@value)) - end - end - end - - def pack - members = [value, expires_at, version] - members.pop while !members.empty? && members.last.nil? - members - end - - private - def uncompress(value) - Marshal.load(Zlib::Inflate.inflate(value)) - end - end end end diff --git a/activesupport/lib/active_support/cache/entry.rb b/activesupport/lib/active_support/cache/entry.rb new file mode 100644 index 0000000000000..f6f514987fdf7 --- /dev/null +++ b/activesupport/lib/active_support/cache/entry.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +require "zlib" + +module ActiveSupport + module Cache + # 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 + + # Creates a new cache entry for the specified value. Options supported are + # +: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 = 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 + + def mismatched?(version) + @version && version && @version != version + end + + # Checks 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 + + def expires_at + @expires_in ? @created_at + @expires_in : nil + end + + def expires_at=(value) + if value + @expires_in = value.to_f - @created_at + else + @expires_in = nil + end + end + + # Returns the size of the cached value. This could be less than + # value.bytesize if the data is compressed. + def bytesize + case value + when NilClass + 0 + when String + @value.bytesize + 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) + @value = @value.dup + else + @value = Marshal.load(Marshal.dump(@value)) + end + end + end + + def pack + members = [value, expires_at, version] + members.pop while !members.empty? && members.last.nil? + members + end + + private + def uncompress(value) + Marshal.load(Zlib::Inflate.inflate(value)) + end + end + end +end diff --git a/activesupport/lib/active_support/cache/serializer_with_fallback.rb b/activesupport/lib/active_support/cache/serializer_with_fallback.rb index 260d82ce317c5..b6015a3eb1db1 100644 --- a/activesupport/lib/active_support/cache/serializer_with_fallback.rb +++ b/activesupport/lib/active_support/cache/serializer_with_fallback.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require "zlib" require "active_support/core_ext/kernel/reporting" module ActiveSupport From 67dd6ee325850447d274bd2d4764faacf6e87f86 Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Wed, 5 Jul 2023 16:10:43 -0500 Subject: [PATCH 5/8] Refactor RedisCacheStore options handling This makes it easier to add new base options with default values to `Cache::Store`. --- .../lib/active_support/cache/redis_cache_store.rb | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/activesupport/lib/active_support/cache/redis_cache_store.rb b/activesupport/lib/active_support/cache/redis_cache_store.rb index 07df9ad4b9932..cbc5130e28ae3 100644 --- a/activesupport/lib/active_support/cache/redis_cache_store.rb +++ b/activesupport/lib/active_support/cache/redis_cache_store.rb @@ -10,6 +10,7 @@ end require "connection_pool" +require "active_support/core_ext/hash/slice" require "active_support/core_ext/numeric/time" require "active_support/digest" @@ -141,7 +142,12 @@ def build_redis_client(**redis_options) # cache.fetch('bar', skip_nil: true) { nil } # cache.exist?('foo') # => true # cache.exist?('bar') # => false - def initialize(namespace: nil, compress: true, compress_threshold: 1.kilobyte, coder: default_coder, expires_in: nil, race_condition_ttl: nil, error_handler: DEFAULT_ERROR_HANDLER, skip_nil: false, **redis_options) + def initialize(error_handler: DEFAULT_ERROR_HANDLER, **redis_options) + base_options = redis_options.extract!( + :coder, :compress, :compress_threshold, + :namespace, :expires_in, :race_condition_ttl, :skip_nil, + ) + if pool_options = self.class.send(:retrieve_pool_options, redis_options) @redis = ::ConnectionPool.new(pool_options) { self.class.build_redis(**redis_options) } else @@ -152,10 +158,7 @@ def initialize(namespace: nil, compress: true, compress_threshold: 1.kilobyte, c @error_handler = error_handler @supports_pipelining = true - super namespace: namespace, - compress: compress, compress_threshold: compress_threshold, - expires_in: expires_in, race_condition_ttl: race_condition_ttl, - coder: coder, skip_nil: skip_nil + super(base_options) end def inspect From 250c3b01df233f85ec71bda633a166628cae8d7e Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Wed, 5 Jul 2023 16:22:19 -0500 Subject: [PATCH 6/8] Add assert_compression helper --- .../cache_store_compression_behavior.rb | 89 ++++++++----------- 1 file changed, 35 insertions(+), 54 deletions(-) diff --git a/activesupport/test/cache/behaviors/cache_store_compression_behavior.rb b/activesupport/test/cache/behaviors/cache_store_compression_behavior.rb index c3bcbb3dbbf90..15920b6702cda 100644 --- a/activesupport/test/cache/behaviors/cache_store_compression_behavior.rb +++ b/activesupport/test/cache/behaviors/cache_store_compression_behavior.rb @@ -8,85 +8,48 @@ module CacheStoreCompressionBehavior included do test "compression by default" do @cache = lookup_store - - assert_uncompressed SMALL_STRING - assert_uncompressed SMALL_OBJECT - if compression_always_disabled_by_default? - assert_uncompressed LARGE_STRING - assert_uncompressed LARGE_OBJECT - else - assert_compressed LARGE_STRING - assert_compressed LARGE_OBJECT - end + assert_compression !compression_always_disabled_by_default? end test "compression can be disabled" do @cache = lookup_store(compress: false) - - assert_uncompressed SMALL_STRING - assert_uncompressed SMALL_OBJECT - assert_uncompressed LARGE_STRING - assert_uncompressed LARGE_OBJECT + assert_compression false end test ":compress method option overrides initializer option" do @cache = lookup_store(compress: true) - - assert_uncompressed SMALL_STRING, compress: false - assert_uncompressed SMALL_OBJECT, compress: false - assert_uncompressed LARGE_STRING, compress: false - assert_uncompressed LARGE_OBJECT, compress: false + assert_compression false, with: { compress: false } @cache = lookup_store(compress: false) - - assert_uncompressed SMALL_STRING, compress: true - assert_uncompressed SMALL_OBJECT, compress: true - assert_compressed LARGE_STRING, compress: true - assert_compressed LARGE_OBJECT, compress: true + assert_compression true, with: { compress: true } end test "low :compress_threshold triggers compression" do @cache = lookup_store(compress: true, compress_threshold: 1) - - assert_compressed SMALL_STRING - assert_compressed SMALL_OBJECT - assert_compressed LARGE_STRING - assert_compressed LARGE_OBJECT + assert_compression :all end test "high :compress_threshold inhibits compression" do @cache = lookup_store(compress: true, compress_threshold: 1.megabyte) - - assert_uncompressed SMALL_STRING - assert_uncompressed SMALL_OBJECT - assert_uncompressed LARGE_STRING - assert_uncompressed LARGE_OBJECT + assert_compression false end test ":compress_threshold method option overrides initializer option" do @cache = lookup_store(compress: true, compress_threshold: 1) - - assert_uncompressed SMALL_STRING, compress_threshold: 1.megabyte - assert_uncompressed SMALL_OBJECT, compress_threshold: 1.megabyte - assert_uncompressed LARGE_STRING, compress_threshold: 1.megabyte - assert_uncompressed LARGE_OBJECT, compress_threshold: 1.megabyte + assert_compression false, with: { compress_threshold: 1.megabyte } @cache = lookup_store(compress: true, compress_threshold: 1.megabyte) - - assert_compressed SMALL_STRING, compress_threshold: 1 - assert_compressed SMALL_OBJECT, compress_threshold: 1 - assert_compressed LARGE_STRING, compress_threshold: 1 - assert_compressed LARGE_OBJECT, compress_threshold: 1 + assert_compression :all, with: { compress_threshold: 1 } end test "compression ignores nil" do - assert_uncompressed nil - assert_uncompressed nil, compress: true, compress_threshold: 1 + assert_not_compress nil + assert_not_compress nil, with: { compress: true, compress_threshold: 1 } end test "compression ignores incompressible data" do - assert_uncompressed "", compress: true, compress_threshold: 1 - assert_uncompressed [*0..127].pack("C*"), compress: true, compress_threshold: 1 + assert_not_compress "", with: { compress: true, compress_threshold: 1 } + assert_not_compress [*0..127].pack("C*"), with: { compress: true, compress_threshold: 1 } end end @@ -99,19 +62,37 @@ module CacheStoreCompressionBehavior SMALL_OBJECT = { data: SMALL_STRING } LARGE_OBJECT = { data: LARGE_STRING } - def assert_compressed(value, **options) + def assert_compress(value, **options) assert_operator compute_entry_size_reduction(value, **options), :>, 0 end - def assert_uncompressed(value, **options) + def assert_not_compress(value, **options) assert_equal 0, compute_entry_size_reduction(value, **options) end - def compute_entry_size_reduction(value, **options) + def assert_compression(compress, **options) + if compress == :all + assert_compress SMALL_STRING, **options + assert_compress SMALL_OBJECT, **options + else + assert_not_compress SMALL_STRING, **options + assert_not_compress SMALL_OBJECT, **options + end + + if compress + assert_compress LARGE_STRING, **options + assert_compress LARGE_OBJECT, **options + else + assert_not_compress LARGE_STRING, **options + assert_not_compress LARGE_OBJECT, **options + end + end + + def compute_entry_size_reduction(value, with: {}) entry = ActiveSupport::Cache::Entry.new(value) - uncompressed = @cache.send(:serialize_entry, entry, **options, compress: false) - actual = @cache.send(:serialize_entry, entry, **options) + uncompressed = @cache.send(:serialize_entry, entry, **with, compress: false) + actual = @cache.send(:serialize_entry, entry, **with) uncompressed.bytesize - actual.bytesize end From bafe1420243ac597ead705df8731b37074cf692e Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Wed, 5 Jul 2023 16:44:19 -0500 Subject: [PATCH 7/8] Test compression with each cache format version --- .../cache_store_compression_behavior.rb | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/activesupport/test/cache/behaviors/cache_store_compression_behavior.rb b/activesupport/test/cache/behaviors/cache_store_compression_behavior.rb index 15920b6702cda..a20737e7d6ba4 100644 --- a/activesupport/test/cache/behaviors/cache_store_compression_behavior.rb +++ b/activesupport/test/cache/behaviors/cache_store_compression_behavior.rb @@ -1,11 +1,32 @@ # frozen_string_literal: true require "active_support/core_ext/numeric/bytes" +require "active_support/core_ext/object/with" module CacheStoreCompressionBehavior extend ActiveSupport::Concern included do + test "compression works with cache format version 6.1 (using Marshal61WithFallback)" do + @cache = with_format(6.1) { lookup_store(compress: true) } + assert_compression true + end + + test "compression works with cache format version 7.0 (using Marshal70WithFallback)" do + @cache = with_format(7.0) { lookup_store(compress: true) } + assert_compression true + end + + test "compression works with cache format version 7.1 (using Marshal71WithFallback)" do + @cache = with_format(7.1) { lookup_store(compress: true) } + assert_compression true + end + + test "compression is disabled with custom coder" do + @cache = with_format(7.1) { lookup_store(coder: Marshal) } + assert_compression false + end + test "compression by default" do @cache = lookup_store assert_compression !compression_always_disabled_by_default? @@ -62,6 +83,12 @@ module CacheStoreCompressionBehavior SMALL_OBJECT = { data: SMALL_STRING } LARGE_OBJECT = { data: LARGE_STRING } + def with_format(format_version, &block) + ActiveSupport.deprecator.silence do + ActiveSupport::Cache.with(format_version: format_version, &block) + end + end + def assert_compress(value, **options) assert_operator compute_entry_size_reduction(value, **options), :>, 0 end From 04bca8b345a17dd1cfb52852da72add5b62d8499 Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Wed, 5 Jul 2023 16:54:07 -0500 Subject: [PATCH 8/8] Test signature for each cache format version --- .../cache_store_format_version_behavior.rb | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/activesupport/test/cache/behaviors/cache_store_format_version_behavior.rb b/activesupport/test/cache/behaviors/cache_store_format_version_behavior.rb index 452a4e10f947f..b111b8cceca6f 100644 --- a/activesupport/test/cache/behaviors/cache_store_format_version_behavior.rb +++ b/activesupport/test/cache/behaviors/cache_store_format_version_behavior.rb @@ -5,7 +5,22 @@ module CacheStoreFormatVersionBehavior extend ActiveSupport::Concern - FORMAT_VERSIONS = [6.1, 7.0, 7.1] + FORMAT_VERSION_SIGNATURES = { + 6.1 => [ + "\x04\x08o".b, # Marshal.dump(entry) + "\x04\x08o".b, # Marshal.dump(entry.compressed(...)) + ], + 7.0 => [ + "\x00\x04\x08[".b, # "\x00" + Marshal.dump(entry.pack) + "\x01\x78".b, # "\x01" + Zlib::Deflate.deflate(...) + ], + 7.1 => [ + "\x00\x04\x08[".b, # "\x00" + Marshal.dump(entry.pack) + "\x01\x78".b, # "\x01" + Zlib::Deflate.deflate(...) + ], + } + + FORMAT_VERSIONS = FORMAT_VERSION_SIGNATURES.keys included do test "format version affects default coder" do @@ -26,6 +41,28 @@ module CacheStoreFormatVersionBehavior end end + FORMAT_VERSION_SIGNATURES.each do |format_version, (uncompressed_signature, compressed_signature)| + test "format version #{format_version.inspect} uses correct signature for uncompressed entries" do + serialized = with_format(format_version) do + lookup_store.send(:serialize_entry, ActiveSupport::Cache::Entry.new(["value"] * 100)) + end + + skip if !serialized.is_a?(String) + + assert_operator serialized, :start_with?, uncompressed_signature + end + + test "format version #{format_version.inspect} uses correct signature for compressed entries" do + serialized = with_format(format_version) do + lookup_store.send(:serialize_entry, ActiveSupport::Cache::Entry.new(["value"] * 100), compress_threshold: 1) + end + + skip if !serialized.is_a?(String) + + assert_operator serialized, :start_with?, compressed_signature + 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