memcached_store 1.2.0 → 2.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: e49987a84d92c00bdc0140eb182b55a7a928f1ed
4
- data.tar.gz: 1a9788d127ef7dbf156c261316f39a1fe90c778c
2
+ SHA256:
3
+ metadata.gz: 35c62775da1377543a0a6720634ffb2a8305f9797f68f953321fe6172601c474
4
+ data.tar.gz: 1a6faa8fbc0e24c90368596627cd0811619be98bc3b752605eef244e99a489fe
5
5
  SHA512:
6
- metadata.gz: 7d1d23e7dbbce68a02bba13ab9a920b834044c2bff3b83497bb723f4b3d0c7a8d31074a47e8b5eb4440b61bb361670795fe30de76ba825320089b968a8c3ec7c
7
- data.tar.gz: b46f2c4de9bd74ff25abd6cdd6ccd986b8df4e99cdc3d9f5294b7ea4d2281c7d6c165bb3fc8629505df4adb9addc1d38f8c9e33c4e9df834ee5275037cac76ff
6
+ metadata.gz: edb29ef36b023d97373270005715f2c734b28779e889dd4a5c3dbf9a7147314198e719d0b296acee7426f621f8157c321c906f1095284809333ac5806e7bc492
7
+ data.tar.gz: e842f684725982fec3815f225a76edaa65c558bec03330fa016382c1acfe16b3dff4f58bde75d1105f5f7395cb438e1cbb9c42b7b5b8a1f5b07b66aa0d85db43
@@ -12,6 +12,16 @@ module ActiveSupport
12
12
  class MemcachedSnappyStore < MemcachedStore
13
13
  class UnsupportedOperation < StandardError; end
14
14
 
15
+ module SnappyCompressor
16
+ def self.compress(source)
17
+ Snappy.deflate(source)
18
+ end
19
+
20
+ def self.decompress(source)
21
+ Snappy.inflate(source)
22
+ end
23
+ end
24
+
15
25
  def increment(*)
16
26
  raise UnsupportedOperation, "increment is not supported by: #{self.class.name}"
17
27
  end
@@ -25,21 +35,10 @@ module ActiveSupport
25
35
  false
26
36
  end
27
37
 
28
- private
29
-
30
- def serialize_entry(entry, options)
31
- value = options[:raw] ? entry.value.to_s : Marshal.dump(entry)
32
- [Snappy.deflate(value), true]
33
- end
34
-
35
- def deserialize_entry(compressed_value)
36
- if compressed_value
37
- super(Snappy.inflate(compressed_value))
38
- end
39
- end
40
-
41
- def cas_raw?(_options)
42
- true
38
+ def initialize(*addresses, **options)
39
+ options[:codec] ||= ActiveSupport::Cache::MemcachedStore::Codec.new(compressor: SnappyCompressor)
40
+ options[:compress] = false
41
+ super(*addresses, **options)
43
42
  end
44
43
  end
45
44
  end
@@ -1,5 +1,6 @@
1
1
  # file havily based out off https://github.com/rails/rails/blob/3-2-stable/activesupport/lib/active_support/cache/mem_cache_store.rb
2
2
  require 'digest/md5'
3
+ require 'delegate'
3
4
 
4
5
  module ActiveSupport
5
6
  module Cache
@@ -13,34 +14,90 @@ module ActiveSupport
13
14
  class MemcachedStore < Store
14
15
  ESCAPE_KEY_CHARS = /[\x00-\x20%\x7F-\xFF]/n
15
16
 
17
+ class Codec
18
+ # use dalli compatible flags
19
+ SERIALIZED_FLAG = 0x1
20
+ COMPRESSED_FLAG = 0x2
21
+
22
+ # Older versions of this gem would use 0 for the flags whether or not
23
+ # the value was marshal dumped. By setting this flag, we can tell if
24
+ # it were set with an older version for backwards compatible decoding.
25
+ RAW_FLAG = 0x10
26
+
27
+ def initialize(serializer: Marshal, compressor: nil)
28
+ @serializer = serializer
29
+ @compressor = compressor
30
+ end
31
+
32
+ def encode(_key, value, flags)
33
+ unless value.is_a?(String)
34
+ flags |= SERIALIZED_FLAG
35
+ value = @serializer.dump(value)
36
+ end
37
+ if @compressor
38
+ flags |= COMPRESSED_FLAG
39
+ value = @compressor.compress(value)
40
+ end
41
+ flags |= RAW_FLAG if flags == 0
42
+ [value, flags]
43
+ end
44
+
45
+ def decode(_key, value, flags)
46
+ if (flags & COMPRESSED_FLAG) != 0
47
+ value = @compressor.decompress(value)
48
+ end
49
+
50
+ if (flags & SERIALIZED_FLAG) != 0
51
+ @serializer.load(value)
52
+ elsif flags == 0 # legacy cache value
53
+ @serializer.load(value) rescue value
54
+ else
55
+ value
56
+ end
57
+ end
58
+ end
59
+
16
60
  attr_accessor :read_only, :swallow_exceptions
17
- attr_writer :on_error
18
61
 
19
- def initialize(*addresses)
62
+ prepend(Strategy::LocalCache)
63
+
64
+ def initialize(*addresses, **options)
20
65
  addresses = addresses.flatten
21
- options = addresses.extract_options!
66
+ options[:codec] ||= Codec.new
22
67
  @swallow_exceptions = true
23
68
  @swallow_exceptions = options.delete(:swallow_exceptions) if options.key?(:swallow_exceptions)
24
69
 
25
- super(options)
70
+ if options.key?(:coder)
71
+ raise ArgumentError, "ActiveSupport::Cache::MemcachedStore doesn't support custom coders"
72
+ end
73
+
74
+ # We don't use a coder, so we set it to nil so Active Support don't think we're using
75
+ # a deprecated one.
76
+ super(options.merge(coder: nil))
26
77
 
27
78
  if addresses.first.is_a?(Memcached)
28
- @data = addresses.first
79
+ @connection = addresses.first
29
80
  raise "Memcached::Rails is no longer supported, "\
30
- "use a Memcached instance instead" if @data.is_a?(Memcached::Rails)
81
+ "use a Memcached instance instead" if @connection.is_a?(Memcached::Rails)
31
82
  else
32
83
  mem_cache_options = options.dup
33
84
  servers = mem_cache_options.delete(:servers)
34
85
  UNIVERSAL_OPTIONS.each { |name| mem_cache_options.delete(name) }
35
- @data = Memcached.new([*addresses, *servers], mem_cache_options)
86
+ @connection = Memcached.new([*addresses, *servers], mem_cache_options)
36
87
  end
37
-
38
- extend Strategy::LocalCache
39
88
  end
40
89
 
41
- def logger
42
- return @logger if defined?(@logger)
43
- @logger = ::Rails.logger if defined?(::Rails)
90
+ def append(name, value, options = nil)
91
+ return true if read_only
92
+ options = merged_options(options)
93
+ normalized_key = normalize_key(name, options)
94
+
95
+ handle_exceptions(return_value_on_error: nil, on_miss: false, miss_exceptions: [Memcached::NotStored]) do
96
+ instrument(:append, name) do
97
+ @connection.append(normalized_key, value)
98
+ end
99
+ true
100
+ end
44
101
  end
45
102
 
46
103
  def write(*)
@@ -63,7 +120,7 @@ module ActiveSupport
63
120
 
64
121
  handle_exceptions(return_value_on_error: {}) do
65
122
  instrument(:read_multi, names, options) do
66
- if raw_values = @data.get(keys_to_names.keys, false)
123
+ if raw_values = @connection.get(keys_to_names.keys)
67
124
  raw_values.each do |key, value|
68
125
  entry = deserialize_entry(value)
69
126
  values[keys_to_names[key]] = entry.value unless entry.expired?
@@ -77,30 +134,40 @@ module ActiveSupport
77
134
  def cas(name, options = nil)
78
135
  options = merged_options(options)
79
136
  key = normalize_key(name, options)
137
+ payload = nil
80
138
 
81
- handle_exceptions(return_value_on_error: false) do
139
+ success = handle_exceptions(return_value_on_error: false) do
82
140
  instrument(:cas, name, options) do
83
- @data.cas(key, expiration(options), !cas_raw?(options)) do |raw_value|
141
+ @connection.cas(key, expiration(options)) do |raw_value|
84
142
  entry = deserialize_entry(raw_value)
85
143
  value = yield entry.value
86
144
  break true if read_only
87
- serialize_entry(Entry.new(value, options), options).first
145
+ payload = serialize_entry(Entry.new(value, **options), options)
88
146
  end
89
147
  end
90
148
  true
91
149
  end
150
+
151
+ if success
152
+ local_cache.write_entry(key, payload) if local_cache
153
+ else
154
+ local_cache.delete_entry(key) if local_cache
155
+ end
156
+
157
+ success
92
158
  end
93
159
 
94
- def cas_multi(*names)
95
- options = names.extract_options!
160
+ def cas_multi(*names, **options)
96
161
  return if names.empty?
97
162
 
98
163
  options = merged_options(options)
99
164
  keys_to_names = Hash[names.map { |name| [normalize_key(name, options), name] }]
100
165
 
166
+ sent_payloads = nil
167
+
101
168
  handle_exceptions(return_value_on_error: false) do
102
169
  instrument(:cas_multi, names, options) do
103
- @data.cas(keys_to_names.keys, expiration(options), !cas_raw?(options)) do |raw_values|
170
+ written_payloads = @connection.cas(keys_to_names.keys, expiration(options)) do |raw_values|
104
171
  values = {}
105
172
 
106
173
  raw_values.each do |key, raw_value|
@@ -113,11 +180,22 @@ module ActiveSupport
113
180
  break true if read_only
114
181
 
115
182
  serialized_values = values.map do |name, value|
116
- [normalize_key(name, options), serialize_entry(Entry.new(value, options), options).first]
183
+ [normalize_key(name, options), serialize_entry(Entry.new(value, **options), options)]
117
184
  end
118
185
 
119
- Hash[serialized_values]
186
+ sent_payloads = Hash[serialized_values]
120
187
  end
188
+
189
+ if local_cache && sent_payloads
190
+ sent_payloads.each_key do |key|
191
+ if written_payloads.key?(key)
192
+ local_cache.write_entry(key, written_payloads[key])
193
+ else
194
+ local_cache.delete_entry(key)
195
+ end
196
+ end
197
+ end
198
+
121
199
  true
122
200
  end
123
201
  end
@@ -127,7 +205,7 @@ module ActiveSupport
127
205
  options = merged_options(options)
128
206
  handle_exceptions(return_value_on_error: nil) do
129
207
  instrument(:increment, name, amount: amount) do
130
- @data.incr(normalize_key(name, options), amount)
208
+ @connection.incr(normalize_key(name, options), amount)
131
209
  end
132
210
  end
133
211
  end
@@ -136,20 +214,20 @@ module ActiveSupport
136
214
  options = merged_options(options)
137
215
  handle_exceptions(return_value_on_error: nil) do
138
216
  instrument(:decrement, name, amount: amount) do
139
- @data.decr(normalize_key(name, options), amount)
217
+ @connection.decr(normalize_key(name, options), amount)
140
218
  end
141
219
  end
142
220
  end
143
221
 
144
222
  def clear(options = nil)
145
223
  ActiveSupport::Notifications.instrument("cache_clear.active_support", options || {}) do
146
- @data.flush
224
+ @connection.flush
147
225
  end
148
226
  end
149
227
 
150
228
  def stats
151
229
  ActiveSupport::Notifications.instrument("cache_stats.active_support") do
152
- @data.stats
230
+ @connection.stats
153
231
  end
154
232
  end
155
233
 
@@ -159,104 +237,156 @@ module ActiveSupport
159
237
 
160
238
  def reset #:nodoc:
161
239
  handle_exceptions(return_value_on_error: false) do
162
- @data.reset
240
+ @connection.reset
163
241
  end
164
242
  end
165
243
 
166
- protected
244
+ private
245
+
246
+ if private_method_defined?(:read_serialized_entry)
247
+ class DupLocalStore < DelegateClass(Strategy::LocalCache::LocalStore)
248
+ def write_entry(_key, entry)
249
+ if entry.is_a?(Entry)
250
+ entry.dup_value!
251
+ end
252
+ super
253
+ end
167
254
 
168
- def read_entry(key, _options) # :nodoc:
169
- handle_exceptions(return_value_on_error: nil) do
170
- deserialize_entry(@data.get(escape_key(key), false))
255
+ def fetch_entry(key)
256
+ entry = super do
257
+ new_entry = yield
258
+ if entry.is_a?(Entry)
259
+ new_entry.dup_value!
260
+ end
261
+ new_entry
262
+ end
263
+ entry = entry.dup
264
+
265
+ if entry.is_a?(Entry)
266
+ entry.dup_value!
267
+ end
268
+
269
+ entry
270
+ end
171
271
  end
172
- end
173
272
 
174
- def write_entry(key, entry, options) # :nodoc:
175
- return true if read_only
176
- method = options && options[:unless_exist] ? :add : :set
177
- expires_in = expiration(options)
178
- value, raw = serialize_entry(entry, options)
179
- handle_exceptions(return_value_on_error: false) do
180
- @data.send(method, escape_key(key), value, expires_in, !raw)
181
- true
273
+ module DupLocalCache
274
+ private
275
+
276
+ def local_cache
277
+ if local_cache = super
278
+ DupLocalStore.new(local_cache)
279
+ end
280
+ end
182
281
  end
183
- end
184
282
 
185
- def delete_entry(key, _options) # :nodoc:
186
- return true if read_only
187
- handle_exceptions(return_value_on_error: false, on_miss: true) do
188
- @data.delete(escape_key(key))
189
- true
283
+ prepend DupLocalCache
284
+
285
+ def read_entry(key, **options) # :nodoc:
286
+ deserialize_entry(read_serialized_entry(key, **options))
190
287
  end
191
- end
192
288
 
193
- private
289
+ def read_serialized_entry(key, **)
290
+ handle_exceptions(return_value_on_error: nil) do
291
+ @connection.get(key)
292
+ end
293
+ end
194
294
 
195
- if ActiveSupport::VERSION::MAJOR < 5
196
- def normalize_key(key, options)
197
- escape_key(namespaced_key(key, options))
295
+ def write_entry(key, entry, **options) # :nodoc:
296
+ return true if read_only
297
+
298
+ write_serialized_entry(key, serialize_entry(entry, **options), **options)
198
299
  end
199
300
 
200
- def escape_key(key)
201
- key = key.to_s.dup
202
- key = key.force_encoding(Encoding::ASCII_8BIT)
203
- key = key.gsub(ESCAPE_KEY_CHARS) { |match| "%#{match.getbyte(0).to_s(16).upcase}" }
204
- key = "#{key[0, 213]}:md5:#{Digest::MD5.hexdigest(key)}" if key.size > 250
205
- key
301
+ def write_serialized_entry(key, value, **options)
302
+ method = options && options[:unless_exist] ? :add : :set
303
+ expires_in = expiration(options)
304
+ handle_exceptions(return_value_on_error: false) do
305
+ @connection.send(method, key, value, expires_in)
306
+ true
307
+ end
206
308
  end
207
309
  else
208
- def normalize_key(key, options)
209
- key = super.dup
210
- key = key.force_encoding(Encoding::ASCII_8BIT)
211
- key = key.gsub(ESCAPE_KEY_CHARS) { |match| "%#{match.getbyte(0).to_s(16).upcase}" }
212
- key = "#{key[0, 213]}:md5:#{Digest::MD5.hexdigest(key)}" if key.size > 250
213
- key
310
+ def read_entry(key, _options) # :nodoc:
311
+ handle_exceptions(return_value_on_error: nil) do
312
+ deserialize_entry(@connection.get(key))
313
+ end
214
314
  end
215
315
 
216
- def escape_key(key)
217
- key
316
+ def write_entry(key, entry, options) # :nodoc:
317
+ return true if read_only
318
+ method = options && options[:unless_exist] ? :add : :set
319
+ expires_in = expiration(options)
320
+ value = serialize_entry(entry, options)
321
+ handle_exceptions(return_value_on_error: false) do
322
+ @connection.send(method, key, value, expires_in)
323
+ true
324
+ end
218
325
  end
219
326
  end
220
327
 
221
- def deserialize_entry(raw_value)
222
- if raw_value
223
- entry = begin
224
- Marshal.load(raw_value)
225
- rescue
226
- raw_value
227
- end
228
- entry.is_a?(Entry) ? entry : Entry.new(entry)
328
+ def delete_entry(key, _options = nil) # :nodoc:
329
+ return true if read_only
330
+ handle_exceptions(return_value_on_error: false, on_miss: true) do
331
+ @connection.delete(key)
332
+ true
229
333
  end
230
334
  end
231
335
 
232
- def serialize_entry(entry, options)
233
- entry = entry.value.to_s if options[:raw]
234
- [entry, options[:raw]]
336
+ private
337
+
338
+ def normalize_key(key, options)
339
+ key = super.dup
340
+ key = key.force_encoding(Encoding::ASCII_8BIT)
341
+ key = key.gsub(ESCAPE_KEY_CHARS) { |match| "%#{match.getbyte(0).to_s(16).upcase}" }
342
+ # When we remove support to Rails 5.1 we can change the code to use ActiveSupport::Digest
343
+ key = "#{key[0, 213]}:md5:#{::Digest::MD5.hexdigest(key)}" if key.size > 250
344
+ key
345
+ end
346
+
347
+ def deserialize_entry(value)
348
+ unless value.nil?
349
+ value.is_a?(Entry) ? value : Entry.new(value, compress: false)
350
+ end
235
351
  end
236
352
 
237
- def cas_raw?(options)
238
- options[:raw]
353
+ def serialize_entry(entry, options = nil)
354
+ if options && options[:raw]
355
+ entry.value.to_s
356
+ else
357
+ entry
358
+ end
239
359
  end
240
360
 
241
361
  def expiration(options)
242
362
  expires_in = options[:expires_in].to_i
243
- if expires_in > 0 && !options[:raw]
244
- # Set the memcache expire a few minutes in the future to support race condition ttls on read
245
- expires_in += 5.minutes.to_i
363
+ if expires_in > 0 && options[:race_condition_ttl] && !options[:raw]
364
+ expires_in += options[:race_condition_ttl].to_i
246
365
  end
247
366
  expires_in
248
367
  end
249
368
 
250
- def handle_exceptions(return_value_on_error:, on_miss: return_value_on_error)
369
+ def handle_exceptions(return_value_on_error:, on_miss: return_value_on_error, miss_exceptions: [])
251
370
  yield
252
- rescue Memcached::NotFound, Memcached::ConnectionDataExists
371
+ rescue Memcached::NotFound, Memcached::ConnectionDataExists, *miss_exceptions
253
372
  on_miss
254
373
  rescue Memcached::Error => e
255
- @on_error.call(e) if @on_error
374
+ log_warning(e)
256
375
  raise unless @swallow_exceptions
257
- logger.warn("memcached error: #{e.class}: #{e.message}") if logger
258
376
  return_value_on_error
259
377
  end
378
+
379
+ def log_warning(err)
380
+ return unless logger
381
+ return if err.is_a?(Memcached::NotStored) && @swallow_exceptions
382
+
383
+ logger.warn(
384
+ "[MEMCACHED_ERROR] swallowed=#{@swallow_exceptions}" \
385
+ " exception_class=#{err.class} exception_message=#{err.message}"
386
+ )
387
+ end
388
+
389
+ ActiveSupport.run_load_hooks(:memcached_store)
260
390
  end
261
391
  end
262
392
  end
@@ -0,0 +1,9 @@
1
+ module MemcachedStore
2
+ class Railtie < Rails::Railtie
3
+ initializer 'memcached_store.configuration' do
4
+ ActiveSupport.on_load(:memcached_store) do
5
+ ActiveSupport::Cache::MemcachedStore.logger ||= Rails.logger
6
+ end
7
+ end
8
+ end
9
+ end
@@ -1,4 +1,4 @@
1
1
  # encoding: utf-8
2
2
  module MemcachedStore
3
- VERSION = "1.2.0"
3
+ VERSION = "2.3.4"
4
4
  end
@@ -1,3 +1,4 @@
1
1
  require "memcached"
2
2
  require "active_support"
3
3
  require "active_support/cache"
4
+ require "memcached_store/railtie" if defined?(Rails)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: memcached_store
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 2.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Camilo Lopez
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2017-12-05 00:00:00.000000000 Z
14
+ date: 2023-10-16 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: activesupport
@@ -19,28 +19,28 @@ dependencies:
19
19
  requirements:
20
20
  - - ">="
21
21
  - !ruby/object:Gem::Version
22
- version: '4'
22
+ version: '6'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
26
26
  requirements:
27
27
  - - ">="
28
28
  - !ruby/object:Gem::Version
29
- version: '4'
29
+ version: '6'
30
30
  - !ruby/object:Gem::Dependency
31
31
  name: memcached
32
32
  requirement: !ruby/object:Gem::Requirement
33
33
  requirements:
34
34
  - - "~>"
35
35
  - !ruby/object:Gem::Version
36
- version: 1.8.0
36
+ version: '1.8'
37
37
  type: :runtime
38
38
  prerelease: false
39
39
  version_requirements: !ruby/object:Gem::Requirement
40
40
  requirements:
41
41
  - - "~>"
42
42
  - !ruby/object:Gem::Version
43
- version: 1.8.0
43
+ version: '1.8'
44
44
  - !ruby/object:Gem::Dependency
45
45
  name: rake
46
46
  requirement: !ruby/object:Gem::Requirement
@@ -55,6 +55,34 @@ dependencies:
55
55
  - - ">="
56
56
  - !ruby/object:Gem::Version
57
57
  version: '0'
58
+ - !ruby/object:Gem::Dependency
59
+ name: mocha
60
+ requirement: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ type: :development
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ - !ruby/object:Gem::Dependency
73
+ name: timecop
74
+ requirement: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ type: :development
80
+ prerelease: false
81
+ version_requirements: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
58
86
  description: Plugin-able Memcached adapters to add features (compression, safety)
59
87
  email:
60
88
  - camilo@camilolopez.com
@@ -70,11 +98,13 @@ files:
70
98
  - lib/active_support/cache/memcached_snappy_store.rb
71
99
  - lib/active_support/cache/memcached_store.rb
72
100
  - lib/memcached_store.rb
101
+ - lib/memcached_store/railtie.rb
73
102
  - lib/memcached_store/version.rb
74
103
  homepage: https://github.com/Shopify/memcached_store/
75
104
  licenses:
76
105
  - MIT
77
- metadata: {}
106
+ metadata:
107
+ allowed_push_host: https://rubygems.org
78
108
  post_install_message:
79
109
  rdoc_options: []
80
110
  require_paths:
@@ -83,15 +113,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
83
113
  requirements:
84
114
  - - ">="
85
115
  - !ruby/object:Gem::Version
86
- version: 2.2.0
116
+ version: 2.6.0
87
117
  required_rubygems_version: !ruby/object:Gem::Requirement
88
118
  requirements:
89
119
  - - ">="
90
120
  - !ruby/object:Gem::Version
91
121
  version: '0'
92
122
  requirements: []
93
- rubyforge_project:
94
- rubygems_version: 2.5.2.1
123
+ rubygems_version: 3.4.20
95
124
  signing_key:
96
125
  specification_version: 4
97
126
  summary: Plugin-able Memcached adapters to add features (compression, safety)