dalli 3.0.4 → 3.1.1

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