dalli 2.7.2 → 3.2.4

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