dalli 2.7.2 → 3.2.4

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