dalli 2.7.11 → 3.2.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of dalli might be problematic. Click here for more details.

Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +7 -6
  3. data/History.md +124 -0
  4. data/README.md +26 -200
  5. data/lib/dalli/cas/client.rb +1 -57
  6. data/lib/dalli/client.rb +230 -263
  7. data/lib/dalli/compressor.rb +12 -2
  8. data/lib/dalli/key_manager.rb +113 -0
  9. data/lib/dalli/options.rb +6 -7
  10. data/lib/dalli/pipelined_getter.rb +177 -0
  11. data/lib/dalli/protocol/base.rb +241 -0
  12. data/lib/dalli/protocol/binary/request_formatter.rb +117 -0
  13. data/lib/dalli/protocol/binary/response_header.rb +36 -0
  14. data/lib/dalli/protocol/binary/response_processor.rb +239 -0
  15. data/lib/dalli/protocol/binary/sasl_authentication.rb +60 -0
  16. data/lib/dalli/protocol/binary.rb +173 -0
  17. data/lib/dalli/protocol/connection_manager.rb +252 -0
  18. data/lib/dalli/protocol/meta/key_regularizer.rb +31 -0
  19. data/lib/dalli/protocol/meta/request_formatter.rb +108 -0
  20. data/lib/dalli/protocol/meta/response_processor.rb +211 -0
  21. data/lib/dalli/protocol/meta.rb +177 -0
  22. data/lib/dalli/protocol/response_buffer.rb +54 -0
  23. data/lib/dalli/protocol/server_config_parser.rb +84 -0
  24. data/lib/dalli/protocol/ttl_sanitizer.rb +45 -0
  25. data/lib/dalli/protocol/value_compressor.rb +85 -0
  26. data/lib/dalli/protocol/value_marshaller.rb +59 -0
  27. data/lib/dalli/protocol/value_serializer.rb +91 -0
  28. data/lib/dalli/protocol.rb +8 -0
  29. data/lib/dalli/ring.rb +90 -81
  30. data/lib/dalli/server.rb +3 -749
  31. data/lib/dalli/servers_arg_normalizer.rb +54 -0
  32. data/lib/dalli/socket.rb +117 -137
  33. data/lib/dalli/version.rb +4 -1
  34. data/lib/dalli.rb +42 -14
  35. data/lib/rack/session/dalli.rb +95 -95
  36. metadata +118 -10
  37. data/lib/action_dispatch/middleware/session/dalli_store.rb +0 -82
  38. data/lib/active_support/cache/dalli_store.rb +0 -441
  39. data/lib/dalli/railtie.rb +0 -8
data/lib/dalli/client.rb CHANGED
@@ -1,17 +1,24 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'digest/md5'
3
4
  require 'set'
4
5
 
5
6
  # encoding: ascii
6
7
  module Dalli
8
+ ##
9
+ # Dalli::Client is the main class which developers will use to interact with
10
+ # Memcached.
11
+ ##
7
12
  class Client
8
-
9
13
  ##
10
14
  # Dalli::Client is the main class which developers will use to interact with
11
15
  # the memcached server. Usage:
12
16
  #
13
- # Dalli::Client.new(['localhost:11211:10', 'cache-2.example.com:11211:5', '192.168.0.1:22122:5', '/var/run/memcached/socket'],
14
- # :threadsafe => true, :failover => true, :expires_in => 300)
17
+ # Dalli::Client.new(['localhost:11211:10',
18
+ # 'cache-2.example.com:11211:5',
19
+ # '192.168.0.1:22122:5',
20
+ # '/var/run/memcached/socket'],
21
+ # failover: true, expires_in: 300)
15
22
  #
16
23
  # servers is an Array of "host:port:weight" where weight allows you to distribute cache unevenly.
17
24
  # Both weight and port are optional. If you pass in nil, Dalli will use the <tt>MEMCACHE_SERVERS</tt>
@@ -24,16 +31,25 @@ module Dalli
24
31
  # - :namespace - prepend each key with this value to provide simple namespacing.
25
32
  # - :failover - if a server is down, look for and store values on another server in the ring. Default: true.
26
33
  # - :threadsafe - ensure that only one thread is actively using a socket at a time. Default: true.
27
- # - :expires_in - default TTL in seconds if you do not pass TTL as a parameter to an individual operation, defaults to 0 or forever
28
- # - :compress - defaults to false, if true Dalli will compress values larger than 1024 bytes before sending them to memcached.
34
+ # - :expires_in - default TTL in seconds if you do not pass TTL as a parameter to an individual operation, defaults
35
+ # to 0 or forever.
36
+ # - :compress - if true Dalli will compress values larger than compression_min_size bytes before sending them
37
+ # to memcached. Default: true.
38
+ # - :compression_min_size - the minimum size (in bytes) for which Dalli will compress values sent to Memcached.
39
+ # Defaults to 4K.
29
40
  # - :serializer - defaults to Marshal
30
- # - :compressor - defaults to zlib
31
- # - :cache_nils - defaults to false, if true Dalli will not treat cached nil values as 'not found' for #fetch operations.
32
- # - :digest_class - defaults to Digest::MD5, allows you to pass in an object that responds to the hexdigest method, useful for injecting a FIPS compliant hash object.
41
+ # - :compressor - defaults to Dalli::Compressor, a Zlib-based implementation
42
+ # - :cache_nils - defaults to false, if true Dalli will not treat cached nil values as 'not found' for
43
+ # #fetch operations.
44
+ # - :digest_class - defaults to Digest::MD5, allows you to pass in an object that responds to the hexdigest method,
45
+ # useful for injecting a FIPS compliant hash object.
46
+ # - :protocol - one of either :binary or :meta, defaulting to :binary. This sets the protocol that Dalli uses
47
+ # to communicate with memcached.
33
48
  #
34
- def initialize(servers=nil, options={})
35
- @servers = normalize_servers(servers || ENV["MEMCACHE_SERVERS"] || '127.0.0.1:11211')
49
+ def initialize(servers = nil, options = {})
50
+ @servers = ::Dalli::ServersArgNormalizer.normalize_servers(servers)
36
51
  @options = normalize_options(options)
52
+ @key_manager = ::Dalli::KeyManager.new(@options)
37
53
  @ring = nil
38
54
  end
39
55
 
@@ -42,22 +58,37 @@ module Dalli
42
58
  #
43
59
 
44
60
  ##
45
- # Turn on quiet aka noreply support.
46
- # All relevant operations within this block will be effectively
47
- # pipelined as Dalli will use 'quiet' operations where possible.
48
- # Currently supports the set, add, replace and delete operations.
49
- def multi
50
- old, Thread.current[:dalli_multi] = Thread.current[:dalli_multi], true
51
- yield
52
- ensure
53
- Thread.current[:dalli_multi] = old
61
+ # Get the value associated with the key.
62
+ # If a value is not found, then +nil+ is returned.
63
+ def get(key, req_options = nil)
64
+ perform(:get, key, req_options)
54
65
  end
55
66
 
56
67
  ##
57
- # Get the value associated with the key.
68
+ # Gat (get and touch) fetch an item and simultaneously update its expiration time.
69
+ #
58
70
  # If a value is not found, then +nil+ is returned.
59
- def get(key, options=nil)
60
- perform(:get, key, options)
71
+ def gat(key, ttl = nil)
72
+ perform(:gat, key, ttl_or_default(ttl))
73
+ end
74
+
75
+ ##
76
+ # Touch updates expiration time for a given key.
77
+ #
78
+ # Returns true if key exists, otherwise nil.
79
+ def touch(key, ttl = nil)
80
+ resp = perform(:touch, key, ttl_or_default(ttl))
81
+ resp.nil? ? nil : true
82
+ end
83
+
84
+ ##
85
+ # Get the value and CAS ID associated with the key. If a block is provided,
86
+ # value and CAS will be passed to the block.
87
+ def get_cas(key)
88
+ (value, cas) = perform(:cas, key)
89
+ return [value, cas] unless block_given?
90
+
91
+ yield value, cas
61
92
  end
62
93
 
63
94
  ##
@@ -65,20 +96,35 @@ module Dalli
65
96
  # If a block is given, yields key/value pairs one at a time.
66
97
  # Otherwise returns a hash of { 'key' => 'value', 'key2' => 'value1' }
67
98
  def get_multi(*keys)
68
- check_keys = keys.flatten
69
- check_keys.compact!
99
+ keys.flatten!
100
+ keys.compact!
101
+
102
+ return {} if keys.empty?
70
103
 
71
- return {} if check_keys.empty?
72
104
  if block_given?
73
- get_multi_yielder(keys) {|k, data| yield k, data.first}
105
+ pipelined_getter.process(keys) { |k, data| yield k, data.first }
74
106
  else
75
- Hash.new.tap do |hash|
76
- get_multi_yielder(keys) {|k, data| hash[k] = data.first}
107
+ {}.tap do |hash|
108
+ pipelined_getter.process(keys) { |k, data| hash[k] = data.first }
77
109
  end
78
110
  end
79
111
  end
80
112
 
81
- CACHE_NILS = {cache_nils: true}.freeze
113
+ ##
114
+ # Fetch multiple keys efficiently, including available metadata such as CAS.
115
+ # If a block is given, yields key/data pairs one a time. Data is an array:
116
+ # [value, cas_id]
117
+ # If no block is given, returns a hash of
118
+ # { 'key' => [value, cas_id] }
119
+ def get_multi_cas(*keys)
120
+ if block_given?
121
+ pipelined_getter.process(keys) { |*args| yield(*args) }
122
+ else
123
+ {}.tap do |hash|
124
+ pipelined_getter.process(keys) { |k, data| hash[k] = data }
125
+ end
126
+ end
127
+ end
82
128
 
83
129
  # Fetch the value associated with the key.
84
130
  # If a value is found, then it is returned.
@@ -88,17 +134,14 @@ module Dalli
88
134
  # If a value is not found (or if the found value is nil and :cache_nils is false)
89
135
  # and a block is given, the block will be invoked and its return value
90
136
  # written to the cache and returned.
91
- def fetch(key, ttl=nil, options=nil)
92
- options = options.nil? ? CACHE_NILS : options.merge(CACHE_NILS) if @options[:cache_nils]
93
- val = get(key, options)
94
- not_found = @options[:cache_nils] ?
95
- val == Dalli::Server::NOT_FOUND :
96
- val.nil?
97
- if not_found && block_given?
98
- val = yield
99
- add(key, val, ttl_or_default(ttl), options)
100
- end
101
- val
137
+ def fetch(key, ttl = nil, req_options = nil)
138
+ req_options = req_options.nil? ? CACHE_NILS : req_options.merge(CACHE_NILS) if cache_nils
139
+ val = get(key, req_options)
140
+ return val unless block_given? && not_found?(val)
141
+
142
+ new_val = yield
143
+ add(key, new_val, ttl_or_default(ttl), req_options)
144
+ new_val
102
145
  end
103
146
 
104
147
  ##
@@ -112,8 +155,8 @@ module Dalli
112
155
  # - nil if the key did not exist.
113
156
  # - false if the value was changed by someone else.
114
157
  # - true if the value was successfully updated.
115
- def cas(key, ttl=nil, options=nil, &block)
116
- cas_core(key, false, ttl, options, &block)
158
+ def cas(key, ttl = nil, req_options = nil, &block)
159
+ cas_core(key, false, ttl, req_options, &block)
117
160
  end
118
161
 
119
162
  ##
@@ -123,30 +166,78 @@ module Dalli
123
166
  # Returns:
124
167
  # - false if the value was changed by someone else.
125
168
  # - true if the value was successfully updated.
126
- def cas!(key, ttl=nil, options=nil, &block)
127
- cas_core(key, true, ttl, options, &block)
169
+ def cas!(key, ttl = nil, req_options = nil, &block)
170
+ cas_core(key, true, ttl, req_options, &block)
128
171
  end
129
172
 
130
- def set(key, value, ttl=nil, options=nil)
131
- perform(:set, key, value, ttl_or_default(ttl), 0, options)
173
+ ##
174
+ # Turn on quiet aka noreply support for a number of
175
+ # memcached operations.
176
+ #
177
+ # All relevant operations within this block will be effectively
178
+ # pipelined as Dalli will use 'quiet' versions. The invoked methods
179
+ # will all return nil, rather than their usual response. Method
180
+ # latency will be substantially lower, as the caller will not be
181
+ # blocking on responses.
182
+ #
183
+ # Currently supports storage (set, add, replace, append, prepend),
184
+ # arithmetic (incr, decr), flush and delete operations. Use of
185
+ # unsupported operations inside a block will raise an error.
186
+ #
187
+ # Any error replies will be discarded at the end of the block, and
188
+ # Dalli client methods invoked inside the block will not
189
+ # have return values
190
+ def quiet
191
+ old = Thread.current[::Dalli::QUIET]
192
+ Thread.current[::Dalli::QUIET] = true
193
+ yield
194
+ ensure
195
+ @ring&.pipeline_consume_and_ignore_responses
196
+ Thread.current[::Dalli::QUIET] = old
197
+ end
198
+ alias multi quiet
199
+
200
+ def set(key, value, ttl = nil, req_options = nil)
201
+ set_cas(key, value, 0, ttl, req_options)
202
+ end
203
+
204
+ ##
205
+ # Set the key-value pair, verifying existing CAS.
206
+ # Returns the resulting CAS value if succeeded, and falsy otherwise.
207
+ def set_cas(key, value, cas, ttl = nil, req_options = nil)
208
+ perform(:set, key, value, ttl_or_default(ttl), cas, req_options)
132
209
  end
133
210
 
134
211
  ##
135
212
  # Conditionally add a key/value pair, if the key does not already exist
136
213
  # on the server. Returns truthy if the operation succeeded.
137
- def add(key, value, ttl=nil, options=nil)
138
- perform(:add, key, value, ttl_or_default(ttl), options)
214
+ def add(key, value, ttl = nil, req_options = nil)
215
+ perform(:add, key, value, ttl_or_default(ttl), req_options)
139
216
  end
140
217
 
141
218
  ##
142
219
  # Conditionally add a key/value pair, only if the key already exists
143
220
  # on the server. Returns truthy if the operation succeeded.
144
- def replace(key, value, ttl=nil, options=nil)
145
- perform(:replace, key, value, ttl_or_default(ttl), 0, options)
221
+ def replace(key, value, ttl = nil, req_options = nil)
222
+ replace_cas(key, value, 0, ttl, req_options)
223
+ end
224
+
225
+ ##
226
+ # Conditionally add a key/value pair, verifying existing CAS, only if the
227
+ # key already exists on the server. Returns the new CAS value if the
228
+ # operation succeeded, or falsy otherwise.
229
+ def replace_cas(key, value, cas, ttl = nil, req_options = nil)
230
+ perform(:replace, key, value, ttl_or_default(ttl), cas, req_options)
231
+ end
232
+
233
+ # Delete a key/value pair, verifying existing CAS.
234
+ # Returns true if succeeded, and falsy otherwise.
235
+ def delete_cas(key, cas = 0)
236
+ perform(:delete, key, cas)
146
237
  end
147
238
 
148
239
  def delete(key)
149
- perform(:delete, key, 0)
240
+ delete_cas(key, 0)
150
241
  end
151
242
 
152
243
  ##
@@ -163,13 +254,6 @@ module Dalli
163
254
  perform(:prepend, key, value.to_s)
164
255
  end
165
256
 
166
- def flush(delay=0)
167
- time = -delay
168
- ring.servers.map { |s| s.request(:flush, time += delay) }
169
- end
170
-
171
- alias_method :flush_all, :flush
172
-
173
257
  ##
174
258
  # Incr adds the given amount to the counter on the memcached server.
175
259
  # Amt must be a positive integer value.
@@ -181,8 +265,11 @@ module Dalli
181
265
  # Note that the ttl will only apply if the counter does not already
182
266
  # exist. To increase an existing counter and update its TTL, use
183
267
  # #cas.
184
- def incr(key, amt=1, ttl=nil, default=nil)
185
- raise ArgumentError, "Positive values only: #{amt}" if amt < 0
268
+ #
269
+ # If the value already exists, it must have been set with raw: true
270
+ def incr(key, amt = 1, ttl = nil, default = nil)
271
+ check_positive!(amt)
272
+
186
273
  perform(:incr, key, amt.to_i, ttl_or_default(ttl), default)
187
274
  end
188
275
 
@@ -200,29 +287,34 @@ module Dalli
200
287
  # Note that the ttl will only apply if the counter does not already
201
288
  # exist. To decrease an existing counter and update its TTL, use
202
289
  # #cas.
203
- def decr(key, amt=1, ttl=nil, default=nil)
204
- raise ArgumentError, "Positive values only: #{amt}" if amt < 0
290
+ #
291
+ # If the value already exists, it must have been set with raw: true
292
+ def decr(key, amt = 1, ttl = nil, default = nil)
293
+ check_positive!(amt)
294
+
205
295
  perform(:decr, key, amt.to_i, ttl_or_default(ttl), default)
206
296
  end
207
297
 
208
298
  ##
209
- # Touch updates expiration time for a given key.
210
- #
211
- # Returns true if key exists, otherwise nil.
212
- def touch(key, ttl=nil)
213
- resp = perform(:touch, key, ttl_or_default(ttl))
214
- resp.nil? ? nil : true
299
+ # Flush the memcached server, at 'delay' seconds in the future.
300
+ # Delay defaults to zero seconds, which means an immediate flush.
301
+ ##
302
+ def flush(delay = 0)
303
+ ring.servers.map { |s| s.request(:flush, delay) }
215
304
  end
305
+ alias flush_all flush
306
+
307
+ ALLOWED_STAT_KEYS = %i[items slabs settings].freeze
216
308
 
217
309
  ##
218
310
  # Collect the stats for each server.
219
311
  # You can optionally pass a type including :items, :slabs or :settings to get specific stats
220
312
  # Returns a hash like { 'hostname:port' => { 'stat1' => 'value1', ... }, 'hostname2:port' => { ... } }
221
- def stats(type=nil)
222
- type = nil if ![nil, :items,:slabs,:settings].include? type
313
+ def stats(type = nil)
314
+ type = nil unless ALLOWED_STAT_KEYS.include? type
223
315
  values = {}
224
316
  ring.servers.each do |server|
225
- values["#{server.name}"] = server.alive? ? server.request(:stats,type.to_s) : nil
317
+ values[server.name.to_s] = server.alive? ? server.request(:stats, type.to_s) : nil
226
318
  end
227
319
  values
228
320
  end
@@ -235,32 +327,40 @@ module Dalli
235
327
  end
236
328
  end
237
329
 
238
- ##
239
- ## Make sure memcache servers are alive, or raise an Dalli::RingError
240
- def alive!
241
- ring.server_for_key("")
242
- end
243
-
244
330
  ##
245
331
  ## Version of the memcache servers.
246
332
  def version
247
333
  values = {}
248
334
  ring.servers.each do |server|
249
- values["#{server.name}"] = server.alive? ? server.request(:version) : nil
335
+ values[server.name.to_s] = server.alive? ? server.request(:version) : nil
250
336
  end
251
337
  values
252
338
  end
253
339
 
340
+ ##
341
+ ## Make sure memcache servers are alive, or raise an Dalli::RingError
342
+ def alive!
343
+ ring.server_for_key('')
344
+ end
345
+
254
346
  ##
255
347
  # Close our connection to each server.
256
348
  # If you perform another operation after this, the connections will be re-established.
257
349
  def close
258
- if @ring
259
- @ring.servers.each { |s| s.close }
260
- @ring = nil
261
- end
350
+ @ring&.close
351
+ @ring = nil
352
+ end
353
+ alias reset close
354
+
355
+ CACHE_NILS = { cache_nils: true }.freeze
356
+
357
+ def not_found?(val)
358
+ cache_nils ? val == ::Dalli::NOT_FOUND : val.nil?
359
+ end
360
+
361
+ def cache_nils
362
+ @options[:cache_nils]
262
363
  end
263
- alias_method :reset, :close
264
364
 
265
365
  # Stub method so a bare Dalli client can pretend to be a connection pool.
266
366
  def with
@@ -269,215 +369,82 @@ module Dalli
269
369
 
270
370
  private
271
371
 
272
- def cas_core(key, always_set, ttl=nil, options=nil)
372
+ def check_positive!(amt)
373
+ raise ArgumentError, "Positive values only: #{amt}" if amt.negative?
374
+ end
375
+
376
+ def cas_core(key, always_set, ttl = nil, req_options = nil)
273
377
  (value, cas) = perform(:cas, key)
274
- value = (!value || value == 'Not found') ? nil : value
275
378
  return if value.nil? && !always_set
379
+
276
380
  newvalue = yield(value)
277
- perform(:set, key, newvalue, ttl_or_default(ttl), cas, options)
381
+ perform(:set, key, newvalue, ttl_or_default(ttl), cas, req_options)
278
382
  end
279
383
 
384
+ ##
385
+ # Uses the argument TTL or the client-wide default. Ensures
386
+ # that the value is an integer
387
+ ##
280
388
  def ttl_or_default(ttl)
281
389
  (ttl || @options[:expires_in]).to_i
282
390
  rescue NoMethodError
283
391
  raise ArgumentError, "Cannot convert ttl (#{ttl}) to an integer"
284
392
  end
285
393
 
286
- def groups_for_keys(*keys)
287
- groups = mapped_keys(keys).flatten.group_by do |key|
288
- begin
289
- ring.server_for_key(key)
290
- rescue Dalli::RingError
291
- Dalli.logger.debug { "unable to get key #{key}" }
292
- nil
293
- end
294
- end
295
- return groups
296
- end
297
-
298
- def mapped_keys(keys)
299
- keys_array = keys.flatten
300
- keys_array.map! { |a| validate_key(a.to_s) }
301
- keys_array
302
- end
303
-
304
- def make_multi_get_requests(groups)
305
- groups.each do |server, keys_for_server|
306
- begin
307
- # TODO: do this with the perform chokepoint?
308
- # But given the fact that fetching the response doesn't take place
309
- # in that slot it's misleading anyway. Need to move all of this method
310
- # into perform to be meaningful
311
- server.request(:send_multiget, keys_for_server)
312
- rescue DalliError, NetworkError => e
313
- Dalli.logger.debug { e.inspect }
314
- Dalli.logger.debug { "unable to get keys for server #{server.name}" }
315
- end
316
- end
317
- end
318
-
319
- def perform_multi_response_start(servers)
320
- servers.each do |server|
321
- next unless server.alive?
322
- begin
323
- server.multi_response_start
324
- rescue DalliError, NetworkError => e
325
- Dalli.logger.debug { e.inspect }
326
- Dalli.logger.debug { "results from this server will be missing" }
327
- servers.delete(server)
328
- end
329
- end
330
- servers
331
- end
332
-
333
- ##
334
- # Normalizes the argument into an array of servers.
335
- # If the argument is a string, or an array containing strings, it's expected that the URIs are comma separated e.g.
336
- # "memcache1.example.com:11211,memcache2.example.com:11211,memcache3.example.com:11211"
337
- def normalize_servers(servers)
338
- Array(servers).flat_map do |server|
339
- if server.is_a? String
340
- server.split(",")
341
- else
342
- server
343
- end
344
- end
345
- end
346
-
347
394
  def ring
395
+ # TODO: This server initialization should probably be pushed down
396
+ # to the Ring
348
397
  @ring ||= Dalli::Ring.new(
349
398
  @servers.map do |s|
350
- server_options = {}
351
- if s =~ %r{\Amemcached://}
352
- uri = URI.parse(s)
353
- server_options[:username] = uri.user
354
- server_options[:password] = uri.password
355
- s = "#{uri.host}:#{uri.port}"
356
- end
357
- Dalli::Server.new(s, @options.merge(server_options))
399
+ protocol_implementation.new(s, @options)
358
400
  end, @options
359
401
  )
360
402
  end
361
403
 
362
- # Chokepoint method for instrumentation
363
- def perform(*all_args)
364
- return yield if block_given?
365
- op, key, *args = *all_args
366
-
367
- key = key.to_s
368
- key = validate_key(key)
369
- begin
370
- server = ring.server_for_key(key)
371
- ret = server.request(op, key, *args)
372
- ret
373
- rescue NetworkError => e
374
- Dalli.logger.debug { e.inspect }
375
- Dalli.logger.debug { "retrying request with new server" }
376
- retry
377
- end
404
+ def protocol_implementation
405
+ @protocol_implementation ||= case @options[:protocol]&.to_s
406
+ when 'meta'
407
+ Dalli::Protocol::Meta
408
+ else
409
+ Dalli::Protocol::Binary
410
+ end
378
411
  end
379
412
 
380
- def validate_key(key)
381
- raise ArgumentError, "key cannot be blank" if !key || key.length == 0
382
- key = key_with_namespace(key)
383
- if key.length > 250
384
- digest_class = @options[:digest_class] || ::Digest::MD5
385
- max_length_before_namespace = 212 - (namespace || '').size
386
- key = "#{key[0, max_length_before_namespace]}:md5:#{digest_class.hexdigest(key)}"
387
- end
388
- return key
389
- end
413
+ ##
414
+ # Chokepoint method for memcached methods with a key argument.
415
+ # Validates the key, resolves the key to the appropriate server
416
+ # instance, and invokes the memcached method on the appropriate
417
+ # server.
418
+ #
419
+ # This method also forces retries on network errors - when
420
+ # a particular memcached instance becomes unreachable, or the
421
+ # operational times out.
422
+ ##
423
+ def perform(*all_args)
424
+ return yield if block_given?
390
425
 
391
- def key_with_namespace(key)
392
- (ns = namespace) ? "#{ns}:#{key}" : key
393
- end
426
+ op, key, *args = all_args
394
427
 
395
- def key_without_namespace(key)
396
- (ns = namespace) ? key.sub(%r(\A#{Regexp.escape ns}:), '') : key
397
- end
428
+ key = key.to_s
429
+ key = @key_manager.validate_key(key)
398
430
 
399
- def namespace
400
- return nil unless @options[:namespace]
401
- @options[:namespace].is_a?(Proc) ? @options[:namespace].call.to_s : @options[:namespace].to_s
431
+ server = ring.server_for_key(key)
432
+ server.request(op, key, *args)
433
+ rescue NetworkError => e
434
+ Dalli.logger.debug { e.inspect }
435
+ Dalli.logger.debug { 'retrying request with new server' }
436
+ retry
402
437
  end
403
438
 
404
439
  def normalize_options(opts)
405
- if opts[:compression]
406
- Dalli.logger.warn "DEPRECATED: Dalli's :compression option is now just :compress => true. Please update your configuration."
407
- opts[:compress] = opts.delete(:compression)
408
- end
409
- begin
410
- opts[:expires_in] = opts[:expires_in].to_i if opts[:expires_in]
411
- rescue NoMethodError
412
- raise ArgumentError, "cannot convert :expires_in => #{opts[:expires_in].inspect} to an integer"
413
- end
414
- if opts[:digest_class] && !opts[:digest_class].respond_to?(:hexdigest)
415
- raise ArgumentError, "The digest_class object must respond to the hexdigest method"
416
- end
440
+ opts[:expires_in] = opts[:expires_in].to_i if opts[:expires_in]
417
441
  opts
442
+ rescue NoMethodError
443
+ raise ArgumentError, "cannot convert :expires_in => #{opts[:expires_in].inspect} to an integer"
418
444
  end
419
445
 
420
- ##
421
- # Yields, one at a time, keys and their values+attributes.
422
- def get_multi_yielder(keys)
423
- perform do
424
- return {} if keys.empty?
425
- ring.lock do
426
- begin
427
- groups = groups_for_keys(keys)
428
- if unfound_keys = groups.delete(nil)
429
- Dalli.logger.debug { "unable to get keys for #{unfound_keys.length} keys because no matching server was found" }
430
- end
431
- make_multi_get_requests(groups)
432
-
433
- servers = groups.keys
434
- return if servers.empty?
435
- servers = perform_multi_response_start(servers)
436
-
437
- start = Time.now
438
- while true
439
- # remove any dead servers
440
- servers.delete_if { |s| s.sock.nil? }
441
- break if servers.empty?
442
-
443
- # calculate remaining timeout
444
- elapsed = Time.now - start
445
- timeout = servers.first.options[:socket_timeout]
446
- time_left = (elapsed > timeout) ? 0 : timeout - elapsed
447
-
448
- sockets = servers.map(&:sock)
449
- readable, _ = IO.select(sockets, nil, nil, time_left)
450
-
451
- if readable.nil?
452
- # no response within timeout; abort pending connections
453
- servers.each do |server|
454
- Dalli.logger.debug { "memcached at #{server.name} did not response within timeout" }
455
- server.multi_response_abort
456
- end
457
- break
458
-
459
- else
460
- readable.each do |sock|
461
- server = sock.server
462
-
463
- begin
464
- server.multi_response_nonblock.each_pair do |key, value_list|
465
- yield key_without_namespace(key), value_list
466
- end
467
-
468
- if server.multi_response_completed?
469
- servers.delete(server)
470
- end
471
- rescue NetworkError
472
- servers.delete(server)
473
- end
474
- end
475
- end
476
- end
477
- end
478
- end
479
- end
446
+ def pipelined_getter
447
+ PipelinedGetter.new(ring, @key_manager)
480
448
  end
481
-
482
449
  end
483
450
  end