diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d99ef66..6417416 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,20 +8,10 @@ jobs: strategy: matrix: entry: - - name: 'Minimum supported' - ruby: '2.6' - gemfile: "Gemfile.min" - - name: 'Latest released' - ruby: '3.0' - gemfile: "Gemfile" - - name: 'Rails edge' - ruby: '3.0' - gemfile: "Gemfile.edge" + - ruby: '2.7' + - ruby: '3.0' - name: ${{ matrix.entry.name }} - - env: - BUNDLE_GEMFILE: ${{ matrix.entry.gemfile }} + name: "Ruby ${{ matrix.entry.ruby }}" services: memcached: diff --git a/Gemfile b/Gemfile index 1466767..d8ccc38 100644 --- a/Gemfile +++ b/Gemfile @@ -2,4 +2,4 @@ source 'https://rubygems.org' gemspec -gem "snappy" +gem "rails", github: "rails/rails", branch: "main" diff --git a/Gemfile.min b/Gemfile.min deleted file mode 100644 index 7c76a42..0000000 --- a/Gemfile.min +++ /dev/null @@ -1,7 +0,0 @@ -source 'https://rubygems.org' - -gemspec - -gem "snappy", "~> 0.2.0" -gem "memcached", "~> 1.8.0" -gem "activesupport", "~> 6.0.0" diff --git a/lib/active_support/cache/memcached_snappy_store.rb b/lib/active_support/cache/memcached_snappy_store.rb deleted file mode 100644 index b579c03..0000000 --- a/lib/active_support/cache/memcached_snappy_store.rb +++ /dev/null @@ -1,45 +0,0 @@ -begin - require 'snappy' -rescue LoadError => e - $stderr.puts "You don't have snappy installed in your application. Please add `gem \"snappy\"` to your Gemfile and run bundle install" - raise e -end - -require 'active_support/cache/memcached_store' - -module ActiveSupport - module Cache - class MemcachedSnappyStore < MemcachedStore - class UnsupportedOperation < StandardError; end - - module SnappyCompressor - def self.compress(source) - Snappy.deflate(source) - end - - def self.decompress(source) - Snappy.inflate(source) - end - end - - def increment(*) - raise UnsupportedOperation, "increment is not supported by: #{self.class.name}" - end - - def decrement(*) - raise UnsupportedOperation, "decrement is not supported by: #{self.class.name}" - end - - # IdentityCache has its own handling for read only. - def read_only - false - end - - def initialize(*addresses, **options) - options[:codec] ||= ActiveSupport::Cache::MemcachedStore::Codec.new(compressor: SnappyCompressor) - options[:compress] = false - super(*addresses, **options) - end - end - end -end diff --git a/lib/active_support/cache/memcached_store.rb b/lib/active_support/cache/memcached_store.rb index 2b64ccf..53b6504 100644 --- a/lib/active_support/cache/memcached_store.rb +++ b/lib/active_support/cache/memcached_store.rb @@ -14,58 +14,21 @@ module Cache class MemcachedStore < Store ESCAPE_KEY_CHARS = /[\x00-\x20%\x7F-\xFF]/n - class Codec - # use dalli compatible flags - SERIALIZED_FLAG = 0x1 - COMPRESSED_FLAG = 0x2 - - # Older versions of this gem would use 0 for the flags whether or not - # the value was marshal dumped. By setting this flag, we can tell if - # it were set with an older version for backwards compatible decoding. - RAW_FLAG = 0x10 - - def initialize(serializer: Marshal, compressor: nil) - @serializer = serializer - @compressor = compressor - end - - def encode(_key, value, flags) - unless value.is_a?(String) - flags |= SERIALIZED_FLAG - value = @serializer.dump(value) - end - if @compressor - flags |= COMPRESSED_FLAG - value = @compressor.compress(value) - end - flags |= RAW_FLAG if flags == 0 + module RawCodec + def self.encode(_key, value, flags) [value, flags] end - def decode(_key, value, flags) - if (flags & COMPRESSED_FLAG) != 0 - value = @compressor.decompress(value) - end - - if (flags & SERIALIZED_FLAG) != 0 - @serializer.load(value) - elsif flags == 0 # legacy cache value - @serializer.load(value) rescue value - else - value - end + def self.decode(_key, value, _flags) + value end end - attr_accessor :read_only, :swallow_exceptions - prepend(Strategy::LocalCache) def initialize(*addresses, **options) addresses = addresses.flatten - options[:codec] ||= Codec.new - @swallow_exceptions = true - @swallow_exceptions = options.delete(:swallow_exceptions) if options.key?(:swallow_exceptions) + options[:codec] ||= RawCodec super(options) @@ -82,7 +45,6 @@ def initialize(*addresses, **options) end def append(name, value, options = nil) - return true if read_only options = merged_options(options) normalized_key = normalize_key(name, options) @@ -94,16 +56,6 @@ def append(name, value, options = nil) end end - def write(*) - return true if read_only - super - end - - def delete(*) - return true if read_only - super - end - def read_multi(*names) options = names.extract_options! return {} if names.empty? @@ -116,7 +68,7 @@ def read_multi(*names) instrument(:read_multi, names, options) do if raw_values = @connection.get(keys_to_names.keys) raw_values.each do |key, value| - entry = deserialize_entry(value) + entry = deserialize_entry(value, **options) values[keys_to_names[key]] = entry.value unless entry.expired? end end @@ -135,7 +87,6 @@ def cas(name, options = nil) @connection.cas(key, expiration(options)) do |raw_value| entry = deserialize_entry(raw_value) value = yield entry.value - break true if read_only payload = serialize_entry(Entry.new(value, **options), options) end end @@ -171,10 +122,8 @@ def cas_multi(*names, **options) values = yield values - break true if read_only - serialized_values = values.map do |name, value| - [normalize_key(name, options), serialize_entry(Entry.new(value, **options), options)] + [normalize_key(name, options), serialize_entry(Entry.new(value, **options), **options)] end sent_payloads = Hash[serialized_values] @@ -237,118 +186,63 @@ def reset #:nodoc: private - if private_method_defined?(:read_serialized_entry) - class DupLocalStore < DelegateClass(Strategy::LocalCache::LocalStore) - def write_entry(_key, entry) - if entry.is_a?(Entry) - entry.dup_value! - end - super - end - - def fetch_entry(key) - entry = super do - new_entry = yield - if entry.is_a?(Entry) - new_entry.dup_value! - end - new_entry - end - entry = entry.dup - - if entry.is_a?(Entry) - entry.dup_value! - end - - entry - end - end - - module DupLocalCache - private - - def local_cache - if local_cache = super - DupLocalStore.new(local_cache) - end - end - end - - prepend DupLocalCache - - def read_entry(key, **options) # :nodoc: - deserialize_entry(read_serialized_entry(key, **options)) - end - - def read_serialized_entry(key, **) - handle_exceptions(return_value_on_error: nil) do - @connection.get(key) - end + def read_entry(key, **options) # :nodoc: + handle_exceptions(return_value_on_error: nil) do + deserialize_entry(read_serialized_entry(key, **options), **options) end + end - def write_entry(key, entry, **options) # :nodoc: - return true if read_only - - write_serialized_entry(key, serialize_entry(entry, **options), **options) + def read_serialized_entry(key, **) + handle_exceptions(return_value_on_error: nil) do + @connection.get(key) end + end - def write_serialized_entry(key, value, **options) - method = options && options[:unless_exist] ? :add : :set - expires_in = expiration(options) - handle_exceptions(return_value_on_error: false) do - @connection.send(method, key, value, expires_in) - true - end - end - else - def read_entry(key, _options) # :nodoc: - handle_exceptions(return_value_on_error: nil) do - deserialize_entry(@connection.get(key)) - end - end + def write_entry(key, entry, **options) # :nodoc: + write_serialized_entry(key, serialize_entry(entry, **options), **options) + end - def write_entry(key, entry, options) # :nodoc: - return true if read_only - method = options && options[:unless_exist] ? :add : :set - expires_in = expiration(options) - value = serialize_entry(entry, options) - handle_exceptions(return_value_on_error: false) do - @connection.send(method, key, value, expires_in) - true - end + def write_serialized_entry(key, value, **options) + method = options && options[:unless_exist] ? :add : :set + expires_in = expiration(options) + handle_exceptions(return_value_on_error: false) do + @connection.send(method, key, value, expires_in) + true end end def delete_entry(key, _options) # :nodoc: - return true if read_only handle_exceptions(return_value_on_error: false, on_miss: true) do @connection.delete(key) true end end - private - def normalize_key(key, options) key = super.dup key = key.force_encoding(Encoding::ASCII_8BIT) key = key.gsub(ESCAPE_KEY_CHARS) { |match| "%#{match.getbyte(0).to_s(16).upcase}" } - # When we remove support to Rails 5.1 we can change the code to use ActiveSupport::Digest key = "#{key[0, 213]}:md5:#{::Digest::MD5.hexdigest(key)}" if key.size > 250 key end - def deserialize_entry(value) - unless value.nil? - value.is_a?(Entry) ? value : Entry.new(value, compress: false) + def deserialize_entry(payload, raw: false, **) + if !payload.nil? && raw + if payload.is_a?(Entry) + payload + else + Entry.new(payload, compress: false) + end + else + super(payload) end end - def serialize_entry(entry, options) - if options[:raw] + def serialize_entry(entry, raw: false, **) + if raw entry.value.to_s else - entry + super(entry) end end @@ -364,19 +258,16 @@ def handle_exceptions(return_value_on_error:, on_miss: return_value_on_error, mi yield rescue Memcached::NotFound, Memcached::ConnectionDataExists, *miss_exceptions on_miss + rescue Memcached::NotStored + return_value_on_error rescue Memcached::Error => e log_warning(e) - raise unless @swallow_exceptions return_value_on_error end def log_warning(err) - return unless logger - return if err.is_a?(Memcached::NotStored) && @swallow_exceptions - - logger.warn( - "[MEMCACHED_ERROR] swallowed=#{@swallow_exceptions}" \ - " exception_class=#{err.class} exception_message=#{err.message}" + logger&.warn( + "[MEMCACHED_ERROR] exception_class=#{err.class} exception_message=#{err.message}" ) end diff --git a/memcached_store.gemspec b/memcached_store.gemspec index d2d1524..2b9c19b 100644 --- a/memcached_store.gemspec +++ b/memcached_store.gemspec @@ -17,9 +17,9 @@ Gem::Specification.new do |spec| spec.files = Dir["lib/**/*.rb", "README.md", "LICENSE"] spec.require_paths = ["lib"] - spec.required_ruby_version = ">= 2.6.0" + spec.required_ruby_version = ">= 2.7.0" - spec.add_runtime_dependency "activesupport", ">= 6" + spec.add_runtime_dependency "activesupport", ">= 7.0.0.alpha" spec.add_runtime_dependency "memcached", "~> 1.8" spec.add_development_dependency "rake" diff --git a/test/support/local_cache_behavior.rb b/test/support/local_cache_behavior.rb index 9603a69..8f706d8 100644 --- a/test/support/local_cache_behavior.rb +++ b/test/support/local_cache_behavior.rb @@ -46,8 +46,6 @@ def test_local_cache_of_write end def test_local_cache_of_read_returns_a_copy_of_the_entry - skip if ActiveSupport.gem_version < Gem::Version.new('6.1') - @cache.with_local_cache do @cache.write(:foo, type: "bar") value = @cache.read(:foo) @@ -71,6 +69,13 @@ def test_local_cache_of_read_nil end end + def test_local_cache_fetch + @cache.with_local_cache do + @cache.send(:local_cache).write_entry "foo", "bar" + assert_equal "bar", @cache.send(:local_cache).fetch_entry("foo") + end + end + def test_local_cache_of_write_nil @cache.with_local_cache do assert @cache.write("foo", nil) @@ -127,7 +132,10 @@ def test_local_cache_of_increment @cache.write("foo", 1, raw: true) @peek.write("foo", 2, raw: true) @cache.increment("foo") - assert_equal 3, Integer(@cache.read("foo", raw: true)) + + expected = @peek.read("foo", raw: true) + assert_equal 3, Integer(expected) + assert_equal expected, @cache.read("foo", raw: true) end end @@ -137,7 +145,9 @@ def test_local_cache_of_decrement @peek.write("foo", 3, raw: true) @cache.decrement("foo") - assert_equal 2, Integer(@cache.read("foo", raw: true)) + expected = @peek.read("foo", raw: true) + assert_equal 2, Integer(expected) + assert_equal expected, @cache.read("foo", raw: true) end end @@ -164,8 +174,6 @@ def test_local_cache_of_read_multi end def test_initial_object_mutation_after_write - skip if ActiveSupport.gem_version < Gem::Version.new('6.1') - @cache.with_local_cache do initial = +"bar" @cache.write("foo", initial) @@ -175,8 +183,6 @@ def test_initial_object_mutation_after_write end def test_initial_object_mutation_after_fetch - skip if ActiveSupport.gem_version < Gem::Version.new('6.1') - @cache.with_local_cache do initial = +"bar" @cache.fetch("foo") { initial } @@ -186,6 +192,17 @@ def test_initial_object_mutation_after_fetch end end + def test_middleware + app = lambda { |env| + result = @cache.write("foo", "bar") + assert_equal "bar", @cache.read("foo") # make sure 'foo' was written + assert result + [200, {}, []] + } + app = @cache.middleware.new(app) + app.call({}) + end + def test_local_race_condition_protection @cache.with_local_cache do time = Time.now diff --git a/test/test_memcached_snappy_store.rb b/test/test_memcached_snappy_store.rb deleted file mode 100644 index cc3c537..0000000 --- a/test/test_memcached_snappy_store.rb +++ /dev/null @@ -1,175 +0,0 @@ -require 'test_helper' - -class TestMemcachedSnappyStore < ActiveSupport::TestCase - setup do - @cache = ActiveSupport::Cache.lookup_store(:memcached_snappy_store, support_cas: true) - @cache.clear - end - - test "test should not allow increment" do - assert_raise(ActiveSupport::Cache::MemcachedSnappyStore::UnsupportedOperation) do - @cache.increment('foo') - end - end - - test "should not allow decrement" do - assert_raise(ActiveSupport::Cache::MemcachedSnappyStore::UnsupportedOperation) do - @cache.decrement('foo') - end - end - - test "write should allow the implicit add operation when unless_exist is passed to write" do - assert_nothing_raised do - @cache.write('foo', 'bar', unless_exist: true) - end - end - - test "should use snappy to write cache entries" do - # Freezing time so created_at is the same in entry and the entry created - # internally and assert_equal between the raw data in the cache and the - # compressed explicitly makes sense - Timecop.freeze do - entry_value = { omg: 'data' } - entry = ActiveSupport::Cache::Entry.new(entry_value) - key = 'moarponies' - assert @cache.write(key, entry_value) - - serialized_entry = Marshal.dump(entry) - serialized_compressed_entry = Snappy.deflate(serialized_entry) - actual_cache_value = @cache.instance_variable_get(:@connection).get(key, false) - - assert_equal serialized_compressed_entry, actual_cache_value - end - end - - test "should use snappy to read cache entries" do - entry_value = { omg: 'data' } - key = 'ponies' - - @cache.write(key, entry_value) - cache_entry = ActiveSupport::Cache::Entry.new(entry_value) - serialized_cached_entry = Marshal.dump(cache_entry) - - Snappy.expects(:inflate).returns(serialized_cached_entry) - assert_equal entry_value, @cache.read(key) - end - - test "should skip snappy to reading not found" do - key = 'ponies2' - Snappy.expects(:inflate).never - assert_nil @cache.read(key) - end - - test "get should work when there is a connection fail" do - key = 'ponies2' - @cache.instance_variable_get(:@connection) - .expects(:check_return_code) - .raises(Memcached::ConnectionFailure) - .at_least_once - - assert_nil @cache.read(key) - end - - test "should use snappy to multi read cache entries but not on missing entries" do - keys = %w(one tow three) - values = keys.map { |k| k * 10 } - entries = values.map { |v| Marshal.dump(ActiveSupport::Cache::Entry.new(v)) } - - keys.each_with_index { |k, i| @cache.write(k, values[i]) } - - keys_and_missing = keys << 'missing' - - Snappy.expects(:inflate).times(3).returns(*entries) - assert_equal values, @cache.read_multi(*keys_and_missing).values - end - - test "should use snappy to multi read cache entries" do - keys = %w(one tow three) - values = keys.map { |k| k * 10 } - entries = values.map { |v| Marshal.dump(ActiveSupport::Cache::Entry.new(v)) } - - keys.each_with_index { |k, i| @cache.write(k, values[i]) } - - Snappy.expects(:inflate).times(3).returns(*entries) - assert_equal values, @cache.read_multi(*keys).values - end - - test "should support raw writes that don't use marshal format" do - key = 'key' - @cache.write(key, 'value', raw: true) - - actual_cache_value = @cache.instance_variable_get(:@connection).get(key, false) - assert_equal 'value', Snappy.inflate(actual_cache_value) - end - - test "cas should use snappy to read and write cache entries" do - entry_value = { omg: 'data' } - update_value = 'value' - key = 'ponies' - - @cache.write(key, entry_value) - result = @cache.cas(key) do |v| - assert_equal entry_value, v - update_value - end - assert result - assert_equal update_value, @cache.read(key) - - actual_cache_value = @cache.instance_variable_get(:@connection).get(key, false) - serialized_entry = Snappy.inflate(actual_cache_value) - entry = Marshal.load(serialized_entry) - assert entry.is_a?(ActiveSupport::Cache::Entry) - assert_equal update_value, entry.value - end - - test "cas should support raw entries that don't use marshal format" do - key = 'key' - @cache.write(key, 'value', raw: true) - result = @cache.cas(key, raw: true) do |v| - assert_equal 'value', v - 'new_value' - end - assert result - actual_cache_value = @cache.instance_variable_get(:@connection).get(key, false) - assert_equal 'new_value', Snappy.inflate(actual_cache_value) - end - - test "cas_multi should use snappy to read and write cache entries" do - keys = %w(one two three four) - values = keys.map { |k| k * 10 } - update_hash = Hash[keys.drop(1).map { |k| [k, k * 11] }] - - keys.zip(values) { |k, v| @cache.write(k, v) } - - result = @cache.cas_multi(*keys) do |hash| - assert_equal Hash[keys.zip(values)], hash - update_hash - end - assert result - assert_equal Hash[keys.zip(values)].merge(update_hash), @cache.read_multi(*keys) - - update_hash.each do |key, value| - actual_cache_value = @cache.instance_variable_get(:@connection).get(key, false) - serialized_entry = Snappy.inflate(actual_cache_value) - entry = Marshal.load(serialized_entry) - assert entry.is_a?(ActiveSupport::Cache::Entry) - assert_equal value, entry.value - end - end - - test "cas_multi should support raw entries that don't use marshal format" do - keys = %w(one two three) - values = keys.map { |k| k * 10 } - update_hash = { "two" => "two" * 11 } - - keys.zip(values) { |k, v| @cache.write(k, v) } - - result = @cache.cas_multi(*keys, raw: true) do |hash| - assert_equal Hash[keys.zip(values)], hash - update_hash - end - assert result - actual_cache_value = @cache.instance_variable_get(:@connection).get("two", false) - assert_equal update_hash["two"], Snappy.inflate(actual_cache_value) - end -end diff --git a/test/test_memcached_store.rb b/test/test_memcached_store.rb index 0d11c96..c8ee5c7 100644 --- a/test/test_memcached_store.rb +++ b/test/test_memcached_store.rb @@ -362,8 +362,8 @@ def test_race_condition_protection_is_safe def test_crazy_key_characters crazy_key = "#/:*(<+=> )&$%@?;'\"\'`~-" assert @cache.write(crazy_key, "1", raw: true) - assert_equal "1", @cache.read(crazy_key) - assert_equal "1", @cache.fetch(crazy_key) + assert_equal "1", @cache.read(crazy_key, raw: true) + assert_equal "1", @cache.fetch(crazy_key, raw: true) assert @cache.delete(crazy_key) assert_equal "2", @cache.fetch(crazy_key, raw: true) { "2" } assert_equal 3, @cache.increment(crazy_key) @@ -383,11 +383,11 @@ def test_really_long_keys def test_increment @cache.write('foo', 1, raw: true) - assert_equal 1, @cache.read('foo').to_i + assert_equal 1, @cache.read('foo', raw: true).to_i assert_equal 2, @cache.increment('foo') - assert_equal 2, @cache.read('foo').to_i + assert_equal 2, @cache.read('foo', raw: true).to_i assert_equal 3, @cache.increment('foo') - assert_equal 3, @cache.read('foo').to_i + assert_equal 3, @cache.read('foo', raw: true).to_i assert_nil @cache.increment('bar') end @@ -398,11 +398,11 @@ def test_increment_not_found def test_decrement @cache.write('foo', 3, raw: true) - assert_equal 3, @cache.read('foo').to_i + assert_equal 3, @cache.read('foo', raw: true).to_i assert_equal 2, @cache.decrement('foo') - assert_equal 2, @cache.read('foo').to_i + assert_equal 2, @cache.read('foo', raw: true).to_i assert_equal 1, @cache.decrement('foo') - assert_equal 1, @cache.read('foo').to_i + assert_equal 1, @cache.read('foo', raw: true).to_i assert_nil @cache.decrement('bar') end @@ -414,8 +414,8 @@ def test_decrement_not_found def test_common_utf8_values key = "\xC3\xBCmlaut".force_encoding(Encoding::UTF_8) assert @cache.write(key, "1", raw: true) - assert_equal "1", @cache.read(key) - assert_equal "1", @cache.fetch(key) + assert_equal "1", @cache.read(key, raw: true) + assert_equal "1", @cache.fetch(key, raw: true) assert @cache.delete(key) assert_equal "2", @cache.fetch(key, raw: true) { "2" } assert_equal 3, @cache.increment(key) @@ -457,191 +457,6 @@ def test_reset @cache.reset end - def test_write_to_read_only_memcached_store_should_not_write - with_read_only(@cache) do - assert @cache.write("walrus", "awesome"), "Writing to a disabled memcached - store should return truthy to make clients not care" - - assert_nil @cache.read("walrus"), "Key should have nil value in disabled cache" - end - end - - def test_delete_with_read_only_memcached_store_should_not_delete - assert @cache.write("walrus", "big") - - with_read_only(@cache) do - assert @cache.delete("walrus"), "Should return truthy when deleted to not raise in client" - end - - assert_equal "big", @cache.read("walrus"), "Cache entry should not have been deleted from read only client" - end - - def test_cas_with_read_only_memcached_store_should_not_s - called_block = false - @cache.write('walrus', 'slimy') - - with_read_only(@cache) do - assert(@cache.cas('walrus') do |value| - assert_equal 'slimy', value - called_block = true - 'full' - end) - end - - assert_equal 'slimy', @cache.read('walrus') - assert called_block, "CAS with read only should have called the inner block with an assertion" - end - - def test_cas_multi_with_read_only_memcached_store_should_not_s - called_block = false - - @cache.write('walrus', 'cool') - @cache.write('narwhal', 'horn') - - with_read_only(@cache) do - assert(@cache.cas_multi('walrus', 'narwhal') do - called_block = true - { "walrus" => "not cool", "narwhal" => "not with horns" } - end) - end - - assert_equal 'cool', @cache.read('walrus') - assert_equal 'horn', @cache.read('narwhal') - assert called_block, "CAS with read only should have called the inner block with an assertion" - end - - def test_write_with_read_only_should_not_send_activesupport_notification - assert_notifications(/cache/, 0) do - with_read_only(@cache) do - assert @cache.write("walrus", "bestest") - end - end - end - - def test_delete_with_read_only_should_not_send_activesupport_notification - assert_notifications(/cache/, 0) do - with_read_only(@cache) do - assert @cache.delete("walrus") - end - end - end - - def test_fetch_with_expires_in_with_read_only_should_not_send_activesupport_notification - expires_in = 10 - @cache.fetch("walrus", expires_in: expires_in) { "yo" } - - Timecop.travel(Time.now + expires_in + 1) do - assert_notifications(/cache_write/, 0) do - with_read_only(@cache) do - @cache.fetch("walrus") { "no" } - end - end - end - end - - def test_fetch_with_expired_entry_with_read_only_should_return_nil_and_not_delete_from_cache - expires_in = 10 - @cache.fetch("walrus", expires_in: expires_in) { "yo" } - - Timecop.travel(Time.now + expires_in + 1) do - with_read_only(@cache) do - value = @cache.fetch("walrus", expires_in: expires_in) { "no" } - - assert_equal "no", value - refute @cache.fetch("walrus"), "Client should return nil for expired key" - assert_equal "yo", @cache.instance_variable_get(:@connection).get("walrus").value - end - end - end - - def test_fetch_with_expired_entry_and_race_condition_ttl_with_read_only_should_return_nil_and_not_delete_from_cache - expires_in = 10 - race_condition_ttl = 10 - @cache.fetch("walrus", expires_in: expires_in) { "yo" } - - Timecop.travel(Time.now + expires_in + 1) do - with_read_only(@cache) do - value = @cache.fetch("walrus", expires_in: expires_in, race_condition_ttl: race_condition_ttl) { "no" } - - assert_equal "no", value - assert_equal "no", @cache.fetch("walrus") { "no" } - refute @cache.fetch("walrus") - - assert_equal "yo", @cache.instance_variable_get(:@connection).get("walrus").value - end - end - end - - def test_read_with_expired_with_read_only_entry_should_return_nil_and_not_delete_from_cache - expires_in = 10 - @cache.fetch("walrus", expires_in: expires_in) { "yo" } - - Timecop.travel(Time.now + expires_in + 1) do - with_read_only(@cache) do - refute @cache.read("walrus") - - assert_equal "yo", @cache.instance_variable_get(:@connection).get("walrus").value - end - end - end - - def test_read_multi_with_expired_entry_should_return_nil_and_not_delete_from_cache - expires_in = 10 - @cache.fetch("walrus", expires_in: expires_in) { "yo" } - @cache.fetch("narwhal", expires_in: expires_in) { "yiir" } - - Timecop.travel(Time.now + expires_in + 1) do - with_read_only(@cache) do - assert_predicate @cache.read_multi("walrus", "narwhal"), :empty? - - assert_equal "yo", @cache.instance_variable_get(:@connection).get("walrus").value - assert_equal "yiir", @cache.instance_variable_get(:@connection).get("narwhal").value - end - end - end - - def test_fetch_with_race_condition_ttl_with_read_only_should_not_send_activesupport_notification - expires_in = 10 - race_condition_ttl = 10 - @cache.fetch("walrus", expires_in: expires_in) { "yo" } - - Timecop.travel(Time.now + expires_in + 1) do - assert_notifications(/cache_write/, 0) do - with_read_only(@cache) do - @cache.fetch("walrus", expires_in: expires_in, race_condition_ttl: race_condition_ttl) { "no" } - end - end - end - end - - def test_cas_with_read_only_should_send_activesupport_notification - @cache.write("walrus", "yes") - - with_read_only(@cache) do - assert_notifications(/cache_cas/, 1) do - assert(@cache.cas("walrus") { |_value| "no" }) - end - end - - assert_equal "yes", @cache.fetch("walrus") - end - - def test_cas_multi_with_read_only_should_send_activesupport_notification - @cache.write("walrus", "yes") - @cache.write("narwhal", "yes") - - with_read_only(@cache) do - assert_notifications(/cache_cas/, 1) do - assert(@cache.cas_multi("walrus", "narwhal") do |*_values| - { "walrus" => "no", "narwhal" => "no" } - end) - end - end - - assert_equal "yes", @cache.fetch("walrus") - assert_equal "yes", @cache.fetch("narwhal") - end - def test_logger_defaults_to_nil assert_nil @cache.logger end @@ -653,24 +468,13 @@ def test_assigns_rails_logger_in_railtie @cache.logger = nil end - def test_constructor_sets_swallow_exceptions - store = ActiveSupport::Cache::MemcachedStore.new([], swallow_exceptions: false) - refute store.swallow_exceptions - end - - def test_read_entry_does_raise_on_error - assert_raises_when_not_swallowing_exceptions do - @cache.read("foo") - end - end - def test_logs_on_error expect_error logger = mock('logger', debug?: false) logger .expects(:warn) - .with("[MEMCACHED_ERROR] swallowed=true exception_class=Memcached::Error exception_message=Memcached::Error") + .with("[MEMCACHED_ERROR] exception_class=Memcached::Error exception_message=Memcached::Error") @cache.logger = logger @cache.read("foo") @@ -678,24 +482,6 @@ def test_logs_on_error @cache.logger = nil end - def test_logs_on_error_when_swallowing_is_disabled - expect_error - - logger = mock('logger', debug?: false) - logger - .expects(:warn) - .with("[MEMCACHED_ERROR] swallowed=false exception_class=Memcached::Error exception_message=Memcached::Error") - - @cache.logger = logger - @cache.swallow_exceptions = false - - assert_raises Memcached::Error do - @cache.read("foo") - end - ensure - @cache.logger = nil - end - def test_does_not_log_on_not_stored_error expect_not_stored @@ -703,86 +489,17 @@ def test_does_not_log_on_not_stored_error logger.expects(:warn).never @cache.logger = logger - @cache.swallow_exceptions = true refute(@cache.write("foo", unless_exist: true)) ensure @cache.logger = nil end - def test_log_not_stored_error_on_exception - expect_not_stored - - logger = mock('logger', debug?: false) - logger.expects(:warn) - .with( - "[MEMCACHED_ERROR] swallowed=false exception_class=Memcached::NotStored exception_message=Memcached::NotStored" - ) - - @cache.logger = logger - @cache.swallow_exceptions = false - - assert_raises Memcached::NotStored do - @cache.write("foo", unless_exist: true) - end - ensure - @cache.logger = nil - end - - def test_read_multi_does_raise_on_error - assert_raises_when_not_swallowing_exceptions do - @cache.read_multi(%w(foo bar)) - end - end - - def test_write_entry_does_raise_on_error - assert_raises_when_not_swallowing_exceptions do - @cache.write("foo", "bar") - end - end - def test_delete_entry_does_not_raise_on_miss expect_not_found - @cache.swallow_exceptions = false @cache.delete("foo") end - def test_delete_entry_does_raise_on_error - assert_raises_when_not_swallowing_exceptions do - @cache.delete("foo") - end - end - - def test_cas_does_raise_on_error - assert_raises_when_not_swallowing_exceptions do - @cache.cas("foo") - end - end - - def test_cas_multi_does_raise_on_error - assert_raises_when_not_swallowing_exceptions do - @cache.cas_multi(%w(foo bar)) - end - end - - def test_increment_does_raise_on_error - assert_raises_when_not_swallowing_exceptions do - @cache.increment("foo") - end - end - - def test_decrement_does_raise_on_error - assert_raises_when_not_swallowing_exceptions do - @cache.decrement("foo") - end - end - - def test_reset_does_raise_on_error - assert_raises_when_not_swallowing_exceptions do - @cache.reset - end - end - def test_append_with_cache_miss assert_equal(false, @cache.append('foo', 'bar')) end @@ -792,46 +509,21 @@ def test_append assert(@cache.append('foo', ',val_2')) end - def test_no_append_in_read_only - assert(@cache.write('foo', 'val_1', raw: true)) - - with_read_only(@cache) do - assert(@cache.append('foo', 'val_2'), 'Should return truthy when appended to not raise in client') - end - - assert_equal('val_1', @cache.read('foo'), 'Cache entry should not have been appended to from read only client') - end - - def test_decoding_keys_written_using_old_version - memcached = Memcached.new - memcached.set("serialized", Marshal.dump(:old), 60, false) - assert_equal :old, @cache.read('serialized') - memcached.set("raw", "old", 60, false) - assert_equal "old", @cache.read('raw') - end - - def test_raw_option_not_needed_on_read - raw_data = Marshal.dump(:raw) - @cache.write("raw", raw_data, raw: true) - assert_equal raw_data, @cache.read('raw') - end - def test_uncompress_regression - limit = if defined? ActiveSupport::Cache::Entry::DEFAULT_COMPRESS_LIMIT - ActiveSupport::Cache::Entry::DEFAULT_COMPRESS_LIMIT - else - ActiveSupport::Cache::DEFAULT_COMPRESS_LIMIT - end - value = "bar" * limit + value = "bar" * ActiveSupport::Cache::DEFAULT_COMPRESS_LIMIT Zlib::Deflate.expects(:deflate).never Zlib::Inflate.expects(:inflate).never @cache.write("foo", value, raw: true, compress: false) - assert_equal(value, @cache.read("foo")) + assert_equal(value, @cache.read("foo", raw: true)) end private + def bypass_read(key) + @cache.send(:deserialize_entry, @cache.instance_variable_get(:@connection).get(key)).value + end + def assert_notifications(pattern, num) count = 0 subscriber = ActiveSupport::Notifications.subscribe(pattern) do |_name, _start, _finish, _id, _payload| @@ -845,22 +537,6 @@ def assert_notifications(pattern, num) ActiveSupport::Notifications.unsubscribe(subscriber) end - def with_read_only(client) - previous = client.read_only - client.read_only = true - yield - ensure - client.read_only = previous - end - - def assert_raises_when_not_swallowing_exceptions - expect_error - @cache.swallow_exceptions = false - assert_raise Memcached::Error do - yield - end - end - def extract_host_port_pairs(servers) servers.map { |host| host.split(':')[0..1].join(':') } end