Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 3 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ source 'https://rubygems.org'

gemspec

gem "snappy"
gem "rails", github: "rails/rails", branch: "main"
7 changes: 0 additions & 7 deletions Gemfile.min

This file was deleted.

45 changes: 0 additions & 45 deletions lib/active_support/cache/memcached_snappy_store.rb

This file was deleted.

189 changes: 40 additions & 149 deletions lib/active_support/cache/memcached_store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)

Expand All @@ -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?
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will break raw values read back from the cache.

Currently a raw value written to the cache with Rails.cache.write("foo", "bar", raw: true) can be read back with just Rails.cache.read("foo") (no raw: true), but with this change this won't work unless you pass raw: true.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I broke this knowingly.

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

Expand All @@ -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

Expand Down
4 changes: 2 additions & 2 deletions memcached_store.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading