dalli 2.7.3 → 3.2.3

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} +211 -0
  3. data/Gemfile +3 -6
  4. data/LICENSE +1 -1
  5. data/README.md +30 -208
  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/pipelined_getter.rb +177 -0
  12. data/lib/dalli/protocol/base.rb +241 -0
  13. data/lib/dalli/protocol/binary/request_formatter.rb +117 -0
  14. data/lib/dalli/protocol/binary/response_header.rb +36 -0
  15. data/lib/dalli/protocol/binary/response_processor.rb +239 -0
  16. data/lib/dalli/protocol/binary/sasl_authentication.rb +60 -0
  17. data/lib/dalli/protocol/binary.rb +173 -0
  18. data/lib/dalli/protocol/connection_manager.rb +252 -0
  19. data/lib/dalli/protocol/meta/key_regularizer.rb +31 -0
  20. data/lib/dalli/protocol/meta/request_formatter.rb +121 -0
  21. data/lib/dalli/protocol/meta/response_processor.rb +211 -0
  22. data/lib/dalli/protocol/meta.rb +178 -0
  23. data/lib/dalli/protocol/response_buffer.rb +54 -0
  24. data/lib/dalli/protocol/server_config_parser.rb +86 -0
  25. data/lib/dalli/protocol/ttl_sanitizer.rb +45 -0
  26. data/lib/dalli/protocol/value_compressor.rb +85 -0
  27. data/lib/dalli/protocol/value_marshaller.rb +59 -0
  28. data/lib/dalli/protocol/value_serializer.rb +91 -0
  29. data/lib/dalli/protocol.rb +8 -0
  30. data/lib/dalli/ring.rb +97 -86
  31. data/lib/dalli/server.rb +4 -719
  32. data/lib/dalli/servers_arg_normalizer.rb +54 -0
  33. data/lib/dalli/socket.rb +123 -115
  34. data/lib/dalli/version.rb +5 -1
  35. data/lib/dalli.rb +45 -14
  36. data/lib/rack/session/dalli.rb +162 -42
  37. metadata +136 -63
  38. data/Performance.md +0 -42
  39. data/Rakefile +0 -43
  40. data/dalli.gemspec +0 -29
  41. data/lib/action_dispatch/middleware/session/dalli_store.rb +0 -81
  42. data/lib/active_support/cache/dalli_store.rb +0 -372
  43. data/lib/dalli/railtie.rb +0 -7
  44. data/test/benchmark_test.rb +0 -243
  45. data/test/helper.rb +0 -56
  46. data/test/memcached_mock.rb +0 -201
  47. data/test/sasl/memcached.conf +0 -1
  48. data/test/sasl/sasldb +0 -1
  49. data/test/test_active_support.rb +0 -541
  50. data/test/test_cas_client.rb +0 -107
  51. data/test/test_compressor.rb +0 -52
  52. data/test/test_dalli.rb +0 -682
  53. data/test/test_encoding.rb +0 -32
  54. data/test/test_failover.rb +0 -137
  55. data/test/test_network.rb +0 -64
  56. data/test/test_rack_session.rb +0 -341
  57. data/test/test_ring.rb +0 -85
  58. data/test/test_sasl.rb +0 -105
  59. data/test/test_serializer.rb +0 -29
  60. 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