dalli 2.7.4 → 3.2.5

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