memcached_store 1.2.0 → 2.3.4

Sign up to get free protection for your applications and to get access to all the features.
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)