dalli 1.1.4 → 2.7.11

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.

data/lib/dalli.rb CHANGED
@@ -1,15 +1,12 @@
1
+ # frozen_string_literal: true
2
+ require 'dalli/compressor'
1
3
  require 'dalli/client'
2
4
  require 'dalli/ring'
3
5
  require 'dalli/server'
4
6
  require 'dalli/socket'
5
7
  require 'dalli/version'
6
8
  require 'dalli/options'
7
-
8
- unless ''.respond_to?(:bytesize)
9
- class String
10
- alias_method :bytesize, :size
11
- end
12
- end
9
+ require 'dalli/railtie' if defined?(::Rails::Railtie)
13
10
 
14
11
  module Dalli
15
12
  # generic error
@@ -18,8 +15,12 @@ module Dalli
18
15
  class NetworkError < DalliError; end
19
16
  # no server available/alive error
20
17
  class RingError < DalliError; end
21
- # application error in marshalling
18
+ # application error in marshalling serialization
22
19
  class MarshalError < DalliError; end
20
+ # application error in marshalling deserialization or decompression
21
+ class UnmarshalError < DalliError; end
22
+ # payload too big for memcached
23
+ class ValueOverMaxSize < DalliError; end
23
24
 
24
25
  def self.logger
25
26
  @logger ||= (rails_logger || default_logger)
@@ -40,6 +41,7 @@ module Dalli
40
41
  def self.logger=(logger)
41
42
  @logger = logger
42
43
  end
44
+
43
45
  end
44
46
 
45
47
  if defined?(RAILS_VERSION) && RAILS_VERSION < '3'
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+ require 'dalli/client'
3
+
4
+ module Dalli
5
+ class Client
6
+ ##
7
+ # Get the value and CAS ID associated with the key. If a block is provided,
8
+ # value and CAS will be passed to the block.
9
+ def get_cas(key)
10
+ (value, cas) = perform(:cas, key)
11
+ value = (!value || value == 'Not found') ? nil : value
12
+ if block_given?
13
+ yield value, cas
14
+ else
15
+ [value, cas]
16
+ end
17
+ end
18
+
19
+ ##
20
+ # Fetch multiple keys efficiently, including available metadata such as CAS.
21
+ # If a block is given, yields key/data pairs one a time. Data is an array:
22
+ # [value, cas_id]
23
+ # If no block is given, returns a hash of
24
+ # { 'key' => [value, cas_id] }
25
+ def get_multi_cas(*keys)
26
+ if block_given?
27
+ get_multi_yielder(keys) {|*args| yield(*args)}
28
+ else
29
+ Hash.new.tap do |hash|
30
+ get_multi_yielder(keys) {|k, data| hash[k] = data}
31
+ end
32
+ end
33
+ end
34
+
35
+ ##
36
+ # Set the key-value pair, verifying existing CAS.
37
+ # Returns the resulting CAS value if succeeded, and falsy otherwise.
38
+ def set_cas(key, value, cas, ttl=nil, options=nil)
39
+ ttl ||= @options[:expires_in].to_i
40
+ perform(:set, key, value, ttl, cas, options)
41
+ end
42
+
43
+ ##
44
+ # Conditionally add a key/value pair, verifying existing CAS, only if the
45
+ # key already exists on the server. Returns the new CAS value if the
46
+ # operation succeeded, or falsy otherwise.
47
+ def replace_cas(key, value, cas, ttl=nil, options=nil)
48
+ ttl ||= @options[:expires_in].to_i
49
+ perform(:replace, key, value, ttl, cas, options)
50
+ end
51
+
52
+ # Delete a key/value pair, verifying existing CAS.
53
+ # Returns true if succeeded, and falsy otherwise.
54
+ def delete_cas(key, cas=0)
55
+ perform(:delete, key, cas)
56
+ end
57
+
58
+ end
59
+ end
data/lib/dalli/client.rb CHANGED
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+ require 'digest/md5'
3
+ require 'set'
4
+
1
5
  # encoding: ascii
2
6
  module Dalli
3
7
  class Client
@@ -6,48 +10,33 @@ module Dalli
6
10
  # Dalli::Client is the main class which developers will use to interact with
7
11
  # the memcached server. Usage:
8
12
  #
9
- # Dalli::Client.new(['localhost:11211:10', 'cache-2.example.com:11211:5', '192.168.0.1:22122:5'],
13
+ # Dalli::Client.new(['localhost:11211:10', 'cache-2.example.com:11211:5', '192.168.0.1:22122:5', '/var/run/memcached/socket'],
10
14
  # :threadsafe => true, :failover => true, :expires_in => 300)
11
15
  #
12
16
  # servers is an Array of "host:port:weight" where weight allows you to distribute cache unevenly.
13
- # Both weight and port are optional. If you pass in nil, Dalli will default to 'localhost:11211'.
14
- # Note that the <tt>MEMCACHE_SERVERS</tt> environment variable will override the servers parameter for use
15
- # in managed environments like Heroku.
16
- #
17
- # You can also provide a Unix socket as an argument, for example:
18
- #
19
- # Dalli::Client.new("/tmp/memcached.sock")
20
- #
21
- # Initial testing shows that Unix sockets are about twice as fast as TCP sockets
22
- # but Unix sockets only work on localhost.
17
+ # Both weight and port are optional. If you pass in nil, Dalli will use the <tt>MEMCACHE_SERVERS</tt>
18
+ # environment variable or default to 'localhost:11211' if it is not present. Dalli also supports
19
+ # the ability to connect to Memcached on localhost through a UNIX socket. To use this functionality,
20
+ # use a full pathname (beginning with a slash character '/') in place of the "host:port" pair in
21
+ # the server configuration.
23
22
  #
24
23
  # Options:
24
+ # - :namespace - prepend each key with this value to provide simple namespacing.
25
25
  # - :failover - if a server is down, look for and store values on another server in the ring. Default: true.
26
26
  # - :threadsafe - ensure that only one thread is actively using a socket at a time. Default: true.
27
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
- # - :compression - defaults to false, if true Dalli will compress values larger than 100 bytes before
29
- # sending them to memcached.
30
- # - :async - assume its running inside the EM reactor. Requires em-synchrony to be installed. Default: false.
28
+ # - :compress - defaults to false, if true Dalli will compress values larger than 1024 bytes before sending them to memcached.
29
+ # - :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.
31
33
  #
32
34
  def initialize(servers=nil, options={})
33
- @servers = env_servers || servers || '127.0.0.1:11211'
34
- @options = { :expires_in => 0 }.merge(options)
35
- self.extend(Dalli::Client::MemcacheClientCompatibility) if Dalli::Client.compatibility_mode
35
+ @servers = normalize_servers(servers || ENV["MEMCACHE_SERVERS"] || '127.0.0.1:11211')
36
+ @options = normalize_options(options)
36
37
  @ring = nil
37
38
  end
38
39
 
39
- ##
40
- # Turn on compatibility mode, which mixes in methods in memcache_client_compatibility.rb
41
- # This value is set to true in memcache-client.rb.
42
- def self.compatibility_mode
43
- @compatibility_mode ||= false
44
- end
45
-
46
- def self.compatibility_mode=(compatibility_mode)
47
- require 'dalli/compatibility'
48
- @compatibility_mode = compatibility_mode
49
- end
50
-
51
40
  #
52
41
  # The standard memcached instruction set
53
42
  #
@@ -64,50 +53,50 @@ module Dalli
64
53
  Thread.current[:dalli_multi] = old
65
54
  end
66
55
 
56
+ ##
57
+ # Get the value associated with the key.
58
+ # If a value is not found, then +nil+ is returned.
67
59
  def get(key, options=nil)
68
- resp = perform(:get, key)
69
- (!resp || resp == 'Not found') ? nil : resp
60
+ perform(:get, key, options)
70
61
  end
71
62
 
72
63
  ##
73
64
  # Fetch multiple keys efficiently.
74
- # Returns a hash of { 'key' => 'value', 'key2' => 'value1' }
65
+ # If a block is given, yields key/value pairs one at a time.
66
+ # Otherwise returns a hash of { 'key' => 'value', 'key2' => 'value1' }
75
67
  def get_multi(*keys)
76
- return {} if keys.empty?
77
- options = nil
78
- options = keys.pop if keys.last.is_a?(Hash) || keys.last.nil?
79
- ring.lock do
80
- keys.flatten.each do |key|
81
- begin
82
- perform(:getkq, key)
83
- rescue DalliError, NetworkError => e
84
- Dalli.logger.debug { e.message }
85
- Dalli.logger.debug { "unable to get key #{key}" }
86
- end
87
- end
68
+ check_keys = keys.flatten
69
+ check_keys.compact!
88
70
 
89
- values = {}
90
- ring.servers.each do |server|
91
- next unless server.alive?
92
- begin
93
- server.request(:noop).each_pair do |key, value|
94
- values[key_without_namespace(key)] = value
95
- end
96
- rescue DalliError, NetworkError => e
97
- Dalli.logger.debug { e.message }
98
- Dalli.logger.debug { "results from this server will be missing" }
99
- end
71
+ return {} if check_keys.empty?
72
+ if block_given?
73
+ get_multi_yielder(keys) {|k, data| yield k, data.first}
74
+ else
75
+ Hash.new.tap do |hash|
76
+ get_multi_yielder(keys) {|k, data| hash[k] = data.first}
100
77
  end
101
- values
102
78
  end
103
79
  end
104
80
 
81
+ CACHE_NILS = {cache_nils: true}.freeze
82
+
83
+ # Fetch the value associated with the key.
84
+ # If a value is found, then it is returned.
85
+ #
86
+ # If a value is not found and no block is given, then nil is returned.
87
+ #
88
+ # If a value is not found (or if the found value is nil and :cache_nils is false)
89
+ # and a block is given, the block will be invoked and its return value
90
+ # written to the cache and returned.
105
91
  def fetch(key, ttl=nil, options=nil)
106
- ttl ||= @options[:expires_in]
92
+ options = options.nil? ? CACHE_NILS : options.merge(CACHE_NILS) if @options[:cache_nils]
107
93
  val = get(key, options)
108
- if val.nil? && block_given?
94
+ not_found = @options[:cache_nils] ?
95
+ val == Dalli::Server::NOT_FOUND :
96
+ val.nil?
97
+ if not_found && block_given?
109
98
  val = yield
110
- add(key, val, ttl, options)
99
+ add(key, val, ttl_or_default(ttl), options)
111
100
  end
112
101
  val
113
102
  end
@@ -124,39 +113,40 @@ module Dalli
124
113
  # - false if the value was changed by someone else.
125
114
  # - true if the value was successfully updated.
126
115
  def cas(key, ttl=nil, options=nil, &block)
127
- ttl ||= @options[:expires_in]
128
- (value, cas) = perform(:cas, key)
129
- value = (!value || value == 'Not found') ? nil : value
130
- if value
131
- newvalue = block.call(value)
132
- perform(:set, key, newvalue, ttl, cas, options)
133
- end
116
+ cas_core(key, false, ttl, options, &block)
117
+ end
118
+
119
+ ##
120
+ # like #cas, but will yield to the block whether or not the value
121
+ # already exists.
122
+ #
123
+ # Returns:
124
+ # - false if the value was changed by someone else.
125
+ # - true if the value was successfully updated.
126
+ def cas!(key, ttl=nil, options=nil, &block)
127
+ cas_core(key, true, ttl, options, &block)
134
128
  end
135
129
 
136
130
  def set(key, value, ttl=nil, options=nil)
137
- raise "Invalid API usage, please require 'dalli/memcache-client' for compatibility, see Upgrade.md" if options == true
138
- ttl ||= @options[:expires_in]
139
- perform(:set, key, value, ttl, 0, options)
131
+ perform(:set, key, value, ttl_or_default(ttl), 0, options)
140
132
  end
141
133
 
142
134
  ##
143
135
  # Conditionally add a key/value pair, if the key does not already exist
144
- # on the server. Returns true if the operation succeeded.
136
+ # on the server. Returns truthy if the operation succeeded.
145
137
  def add(key, value, ttl=nil, options=nil)
146
- ttl ||= @options[:expires_in]
147
- perform(:add, key, value, ttl, options)
138
+ perform(:add, key, value, ttl_or_default(ttl), options)
148
139
  end
149
140
 
150
141
  ##
151
142
  # Conditionally add a key/value pair, only if the key already exists
152
- # on the server. Returns true if the operation succeeded.
143
+ # on the server. Returns truthy if the operation succeeded.
153
144
  def replace(key, value, ttl=nil, options=nil)
154
- ttl ||= @options[:expires_in]
155
- perform(:replace, key, value, ttl, options)
145
+ perform(:replace, key, value, ttl_or_default(ttl), 0, options)
156
146
  end
157
147
 
158
148
  def delete(key)
159
- perform(:delete, key)
149
+ perform(:delete, key, 0)
160
150
  end
161
151
 
162
152
  ##
@@ -178,12 +168,11 @@ module Dalli
178
168
  ring.servers.map { |s| s.request(:flush, time += delay) }
179
169
  end
180
170
 
181
- # deprecated, please use #flush.
182
171
  alias_method :flush_all, :flush
183
172
 
184
173
  ##
185
174
  # Incr adds the given amount to the counter on the memcached server.
186
- # Amt must be a positive value.
175
+ # Amt must be a positive integer value.
187
176
  #
188
177
  # If default is nil, the counter must already exist or the operation
189
178
  # will fail and will return nil. Otherwise this method will return
@@ -194,13 +183,12 @@ module Dalli
194
183
  # #cas.
195
184
  def incr(key, amt=1, ttl=nil, default=nil)
196
185
  raise ArgumentError, "Positive values only: #{amt}" if amt < 0
197
- ttl ||= @options[:expires_in]
198
- perform(:incr, key, amt, ttl, default)
186
+ perform(:incr, key, amt.to_i, ttl_or_default(ttl), default)
199
187
  end
200
188
 
201
189
  ##
202
190
  # Decr subtracts the given amount from the counter on the memcached server.
203
- # Amt must be a positive value.
191
+ # Amt must be a positive integer value.
204
192
  #
205
193
  # memcached counters are unsigned and cannot hold negative values. Calling
206
194
  # decr on a counter which is 0 will just return 0.
@@ -214,17 +202,51 @@ module Dalli
214
202
  # #cas.
215
203
  def decr(key, amt=1, ttl=nil, default=nil)
216
204
  raise ArgumentError, "Positive values only: #{amt}" if amt < 0
217
- ttl ||= @options[:expires_in]
218
- perform(:decr, key, amt, ttl, default)
205
+ perform(:decr, key, amt.to_i, ttl_or_default(ttl), default)
206
+ end
207
+
208
+ ##
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
219
215
  end
220
216
 
221
217
  ##
222
218
  # Collect the stats for each server.
219
+ # You can optionally pass a type including :items, :slabs or :settings to get specific stats
223
220
  # Returns a hash like { 'hostname:port' => { 'stat1' => 'value1', ... }, 'hostname2:port' => { ... } }
224
- def stats
221
+ def stats(type=nil)
222
+ type = nil if ![nil, :items,:slabs,:settings].include? type
225
223
  values = {}
226
224
  ring.servers.each do |server|
227
- values["#{server.hostname}:#{server.port}"] = server.alive? ? server.request(:stats) : nil
225
+ values["#{server.name}"] = server.alive? ? server.request(:stats,type.to_s) : nil
226
+ end
227
+ values
228
+ end
229
+
230
+ ##
231
+ # Reset stats for each server.
232
+ def reset_stats
233
+ ring.servers.map do |server|
234
+ server.alive? ? server.request(:reset_stats) : nil
235
+ end
236
+ end
237
+
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
+ ##
245
+ ## Version of the memcache servers.
246
+ def version
247
+ values = {}
248
+ ring.servers.each do |server|
249
+ values["#{server.name}"] = server.alive? ? server.request(:version) : nil
228
250
  end
229
251
  values
230
252
  end
@@ -240,49 +262,222 @@ module Dalli
240
262
  end
241
263
  alias_method :reset, :close
242
264
 
265
+ # Stub method so a bare Dalli client can pretend to be a connection pool.
266
+ def with
267
+ yield self
268
+ end
269
+
243
270
  private
244
271
 
272
+ def cas_core(key, always_set, ttl=nil, options=nil)
273
+ (value, cas) = perform(:cas, key)
274
+ value = (!value || value == 'Not found') ? nil : value
275
+ return if value.nil? && !always_set
276
+ newvalue = yield(value)
277
+ perform(:set, key, newvalue, ttl_or_default(ttl), cas, options)
278
+ end
279
+
280
+ def ttl_or_default(ttl)
281
+ (ttl || @options[:expires_in]).to_i
282
+ rescue NoMethodError
283
+ raise ArgumentError, "Cannot convert ttl (#{ttl}) to an integer"
284
+ end
285
+
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
+
245
347
  def ring
246
348
  @ring ||= Dalli::Ring.new(
247
- Array(@servers).map do |s|
248
- Dalli::Server.new(s, @options)
349
+ @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))
249
358
  end, @options
250
359
  )
251
360
  end
252
361
 
253
- def env_servers
254
- ENV['MEMCACHE_SERVERS'] ? ENV['MEMCACHE_SERVERS'].split(',') : nil
255
- end
256
-
257
362
  # Chokepoint method for instrumentation
258
- def perform(op, key, *args)
363
+ def perform(*all_args)
364
+ return yield if block_given?
365
+ op, key, *args = *all_args
366
+
259
367
  key = key.to_s
260
- validate_key(key)
261
- key = key_with_namespace(key)
368
+ key = validate_key(key)
262
369
  begin
263
370
  server = ring.server_for_key(key)
264
- server.request(op, key, *args)
371
+ ret = server.request(op, key, *args)
372
+ ret
265
373
  rescue NetworkError => e
266
- Dalli.logger.debug { e.message }
374
+ Dalli.logger.debug { e.inspect }
267
375
  Dalli.logger.debug { "retrying request with new server" }
268
376
  retry
269
377
  end
270
378
  end
271
379
 
272
380
  def validate_key(key)
273
- raise ArgumentError, "illegal character in key #{key}" if key.respond_to?(:ascii_only?) && !key.ascii_only?
274
- raise ArgumentError, "illegal character in key #{key}" if key =~ /\s/
275
- raise ArgumentError, "illegal character in key #{key}" if key =~ /[\x00-\x20\x80-\xFF]/
276
- raise ArgumentError, "key cannot be blank" if key.nil? || key.strip.size == 0
277
- raise ArgumentError, "key too long #{key.inspect}" if key.length > 250
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
278
389
  end
279
390
 
280
391
  def key_with_namespace(key)
281
- @options[:namespace] ? "#{@options[:namespace]}:#{key}" : key
392
+ (ns = namespace) ? "#{ns}:#{key}" : key
282
393
  end
283
394
 
284
395
  def key_without_namespace(key)
285
- @options[:namespace] ? key.gsub(%r(\A#{@options[:namespace]}:), '') : key
396
+ (ns = namespace) ? key.sub(%r(\A#{Regexp.escape ns}:), '') : key
286
397
  end
398
+
399
+ def namespace
400
+ return nil unless @options[:namespace]
401
+ @options[:namespace].is_a?(Proc) ? @options[:namespace].call.to_s : @options[:namespace].to_s
402
+ end
403
+
404
+ 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
417
+ opts
418
+ end
419
+
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
480
+ end
481
+
287
482
  end
288
483
  end