dalli 2.7.8 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of dalli might be problematic. Click here for more details.

data/lib/dalli/client.rb CHANGED
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
- require 'digest/md5'
3
- require 'set'
2
+
3
+ require "digest/md5"
4
+ require "set"
4
5
 
5
6
  # encoding: ascii
6
7
  module Dalli
7
8
  class Client
8
-
9
9
  ##
10
10
  # Dalli::Client is the main class which developers will use to interact with
11
11
  # the memcached server. Usage:
@@ -29,9 +29,12 @@ module Dalli
29
29
  # - :serializer - defaults to Marshal
30
30
  # - :compressor - defaults to zlib
31
31
  # - :cache_nils - defaults to false, if true Dalli will not treat cached nil values as 'not found' for #fetch operations.
32
+ # - :digest_class - defaults to Digest::MD5, allows you to pass in an object that responds to the hexdigest method, useful for injecting a FIPS compliant hash object.
33
+ # - :protocol_implementation - defaults to Dalli::Protocol::Binary which uses the binary protocol. Allows you to pass an alternative implementation using another protocol.
32
34
  #
33
- def initialize(servers=nil, options={})
34
- @servers = normalize_servers(servers || ENV["MEMCACHE_SERVERS"] || '127.0.0.1:11211')
35
+ def initialize(servers = nil, options = {})
36
+ validate_servers_arg(servers)
37
+ @servers = normalize_servers(servers || ENV["MEMCACHE_SERVERS"] || "127.0.0.1:11211")
35
38
  @options = normalize_options(options)
36
39
  @ring = nil
37
40
  end
@@ -55,7 +58,7 @@ module Dalli
55
58
  ##
56
59
  # Get the value associated with the key.
57
60
  # If a value is not found, then +nil+ is returned.
58
- def get(key, options=nil)
61
+ def get(key, options = nil)
59
62
  perform(:get, key, options)
60
63
  end
61
64
 
@@ -64,12 +67,15 @@ module Dalli
64
67
  # If a block is given, yields key/value pairs one at a time.
65
68
  # Otherwise returns a hash of { 'key' => 'value', 'key2' => 'value1' }
66
69
  def get_multi(*keys)
67
- return {} if keys.flatten.compact.empty?
70
+ keys.flatten!
71
+ keys.compact!
72
+
73
+ return {} if keys.empty?
68
74
  if block_given?
69
- get_multi_yielder(keys) {|k, data| yield k, data.first}
75
+ get_multi_yielder(keys) { |k, data| yield k, data.first }
70
76
  else
71
- Hash.new.tap do |hash|
72
- get_multi_yielder(keys) {|k, data| hash[k] = data.first}
77
+ {}.tap do |hash|
78
+ get_multi_yielder(keys) { |k, data| hash[k] = data.first }
73
79
  end
74
80
  end
75
81
  end
@@ -84,11 +90,11 @@ module Dalli
84
90
  # If a value is not found (or if the found value is nil and :cache_nils is false)
85
91
  # and a block is given, the block will be invoked and its return value
86
92
  # written to the cache and returned.
87
- def fetch(key, ttl=nil, options=nil)
93
+ def fetch(key, ttl = nil, options = nil)
88
94
  options = options.nil? ? CACHE_NILS : options.merge(CACHE_NILS) if @options[:cache_nils]
89
95
  val = get(key, options)
90
96
  not_found = @options[:cache_nils] ?
91
- val == Dalli::Server::NOT_FOUND :
97
+ val == Dalli::Protocol::NOT_FOUND :
92
98
  val.nil?
93
99
  if not_found && block_given?
94
100
  val = yield
@@ -108,7 +114,7 @@ module Dalli
108
114
  # - nil if the key did not exist.
109
115
  # - false if the value was changed by someone else.
110
116
  # - true if the value was successfully updated.
111
- def cas(key, ttl=nil, options=nil, &block)
117
+ def cas(key, ttl = nil, options = nil, &block)
112
118
  cas_core(key, false, ttl, options, &block)
113
119
  end
114
120
 
@@ -119,25 +125,25 @@ module Dalli
119
125
  # Returns:
120
126
  # - false if the value was changed by someone else.
121
127
  # - true if the value was successfully updated.
122
- def cas!(key, ttl=nil, options=nil, &block)
128
+ def cas!(key, ttl = nil, options = nil, &block)
123
129
  cas_core(key, true, ttl, options, &block)
124
130
  end
125
131
 
126
- def set(key, value, ttl=nil, options=nil)
132
+ def set(key, value, ttl = nil, options = nil)
127
133
  perform(:set, key, value, ttl_or_default(ttl), 0, options)
128
134
  end
129
135
 
130
136
  ##
131
137
  # Conditionally add a key/value pair, if the key does not already exist
132
138
  # on the server. Returns truthy if the operation succeeded.
133
- def add(key, value, ttl=nil, options=nil)
139
+ def add(key, value, ttl = nil, options = nil)
134
140
  perform(:add, key, value, ttl_or_default(ttl), options)
135
141
  end
136
142
 
137
143
  ##
138
144
  # Conditionally add a key/value pair, only if the key already exists
139
145
  # on the server. Returns truthy if the operation succeeded.
140
- def replace(key, value, ttl=nil, options=nil)
146
+ def replace(key, value, ttl = nil, options = nil)
141
147
  perform(:replace, key, value, ttl_or_default(ttl), 0, options)
142
148
  end
143
149
 
@@ -159,7 +165,7 @@ module Dalli
159
165
  perform(:prepend, key, value.to_s)
160
166
  end
161
167
 
162
- def flush(delay=0)
168
+ def flush(delay = 0)
163
169
  time = -delay
164
170
  ring.servers.map { |s| s.request(:flush, time += delay) }
165
171
  end
@@ -177,7 +183,7 @@ module Dalli
177
183
  # Note that the ttl will only apply if the counter does not already
178
184
  # exist. To increase an existing counter and update its TTL, use
179
185
  # #cas.
180
- def incr(key, amt=1, ttl=nil, default=nil)
186
+ def incr(key, amt = 1, ttl = nil, default = nil)
181
187
  raise ArgumentError, "Positive values only: #{amt}" if amt < 0
182
188
  perform(:incr, key, amt.to_i, ttl_or_default(ttl), default)
183
189
  end
@@ -196,7 +202,7 @@ module Dalli
196
202
  # Note that the ttl will only apply if the counter does not already
197
203
  # exist. To decrease an existing counter and update its TTL, use
198
204
  # #cas.
199
- def decr(key, amt=1, ttl=nil, default=nil)
205
+ def decr(key, amt = 1, ttl = nil, default = nil)
200
206
  raise ArgumentError, "Positive values only: #{amt}" if amt < 0
201
207
  perform(:decr, key, amt.to_i, ttl_or_default(ttl), default)
202
208
  end
@@ -205,20 +211,28 @@ module Dalli
205
211
  # Touch updates expiration time for a given key.
206
212
  #
207
213
  # Returns true if key exists, otherwise nil.
208
- def touch(key, ttl=nil)
214
+ def touch(key, ttl = nil)
209
215
  resp = perform(:touch, key, ttl_or_default(ttl))
210
216
  resp.nil? ? nil : true
211
217
  end
212
218
 
219
+ ##
220
+ # Gat (get and touch) fetch an item and simultaneously update its expiration time.
221
+ #
222
+ # If a value is not found, then +nil+ is returned.
223
+ def gat(key, ttl = nil)
224
+ perform(:gat, key, ttl_or_default(ttl))
225
+ end
226
+
213
227
  ##
214
228
  # Collect the stats for each server.
215
229
  # You can optionally pass a type including :items, :slabs or :settings to get specific stats
216
230
  # Returns a hash like { 'hostname:port' => { 'stat1' => 'value1', ... }, 'hostname2:port' => { ... } }
217
- def stats(type=nil)
218
- type = nil if ![nil, :items,:slabs,:settings].include? type
231
+ def stats(type = nil)
232
+ type = nil unless [nil, :items, :slabs, :settings].include? type
219
233
  values = {}
220
234
  ring.servers.each do |server|
221
- values["#{server.name}"] = server.alive? ? server.request(:stats,type.to_s) : nil
235
+ values[server.name.to_s] = server.alive? ? server.request(:stats, type.to_s) : nil
222
236
  end
223
237
  values
224
238
  end
@@ -242,11 +256,63 @@ module Dalli
242
256
  def version
243
257
  values = {}
244
258
  ring.servers.each do |server|
245
- values["#{server.name}"] = server.alive? ? server.request(:version) : nil
259
+ values[server.name.to_s] = server.alive? ? server.request(:version) : nil
246
260
  end
247
261
  values
248
262
  end
249
263
 
264
+ ##
265
+ # Get the value and CAS ID associated with the key. If a block is provided,
266
+ # value and CAS will be passed to the block.
267
+ def get_cas(key)
268
+ (value, cas) = perform(:cas, key)
269
+ value = !value || value == "Not found" ? nil : value
270
+ if block_given?
271
+ yield value, cas
272
+ else
273
+ [value, cas]
274
+ end
275
+ end
276
+
277
+ ##
278
+ # Fetch multiple keys efficiently, including available metadata such as CAS.
279
+ # If a block is given, yields key/data pairs one a time. Data is an array:
280
+ # [value, cas_id]
281
+ # If no block is given, returns a hash of
282
+ # { 'key' => [value, cas_id] }
283
+ def get_multi_cas(*keys)
284
+ if block_given?
285
+ get_multi_yielder(keys) { |*args| yield(*args) }
286
+ else
287
+ {}.tap do |hash|
288
+ get_multi_yielder(keys) { |k, data| hash[k] = data }
289
+ end
290
+ end
291
+ end
292
+
293
+ ##
294
+ # Set the key-value pair, verifying existing CAS.
295
+ # Returns the resulting CAS value if succeeded, and falsy otherwise.
296
+ def set_cas(key, value, cas, ttl = nil, options = nil)
297
+ ttl ||= @options[:expires_in].to_i
298
+ perform(:set, key, value, ttl, cas, options)
299
+ end
300
+
301
+ ##
302
+ # Conditionally add a key/value pair, verifying existing CAS, only if the
303
+ # key already exists on the server. Returns the new CAS value if the
304
+ # operation succeeded, or falsy otherwise.
305
+ def replace_cas(key, value, cas, ttl = nil, options = nil)
306
+ ttl ||= @options[:expires_in].to_i
307
+ perform(:replace, key, value, ttl, cas, options)
308
+ end
309
+
310
+ # Delete a key/value pair, verifying existing CAS.
311
+ # Returns true if succeeded, and falsy otherwise.
312
+ def delete_cas(key, cas = 0)
313
+ perform(:delete, key, cas)
314
+ end
315
+
250
316
  ##
251
317
  # Close our connection to each server.
252
318
  # If you perform another operation after this, the connections will be re-established.
@@ -258,16 +324,11 @@ module Dalli
258
324
  end
259
325
  alias_method :reset, :close
260
326
 
261
- # Stub method so a bare Dalli client can pretend to be a connection pool.
262
- def with
263
- yield self
264
- end
265
-
266
327
  private
267
328
 
268
- def cas_core(key, always_set, ttl=nil, options=nil)
329
+ def cas_core(key, always_set, ttl = nil, options = nil)
269
330
  (value, cas) = perform(:cas, key)
270
- value = (!value || value == 'Not found') ? nil : value
331
+ value = !value || value == "Not found" ? nil : value
271
332
  return if value.nil? && !always_set
272
333
  newvalue = yield(value)
273
334
  perform(:set, key, newvalue, ttl_or_default(ttl), cas, options)
@@ -280,33 +341,29 @@ module Dalli
280
341
  end
281
342
 
282
343
  def groups_for_keys(*keys)
283
- groups = mapped_keys(keys).flatten.group_by do |key|
344
+ keys.flatten!
345
+ keys.map! { |a| validate_key(a.to_s) }
346
+
347
+ keys.group_by { |key|
284
348
  begin
285
349
  ring.server_for_key(key)
286
350
  rescue Dalli::RingError
287
351
  Dalli.logger.debug { "unable to get key #{key}" }
288
352
  nil
289
353
  end
290
- end
291
- return groups
292
- end
293
-
294
- def mapped_keys(keys)
295
- keys.flatten.map {|a| validate_key(a.to_s)}
354
+ }
296
355
  end
297
356
 
298
357
  def make_multi_get_requests(groups)
299
358
  groups.each do |server, keys_for_server|
300
- begin
301
- # TODO: do this with the perform chokepoint?
302
- # But given the fact that fetching the response doesn't take place
303
- # in that slot it's misleading anyway. Need to move all of this method
304
- # into perform to be meaningful
305
- server.request(:send_multiget, keys_for_server)
306
- rescue DalliError, NetworkError => e
307
- Dalli.logger.debug { e.inspect }
308
- Dalli.logger.debug { "unable to get keys for server #{server.name}" }
309
- end
359
+ # TODO: do this with the perform chokepoint?
360
+ # But given the fact that fetching the response doesn't take place
361
+ # in that slot it's misleading anyway. Need to move all of this method
362
+ # into perform to be meaningful
363
+ server.request(:send_multiget, keys_for_server)
364
+ rescue DalliError, NetworkError => e
365
+ Dalli.logger.debug { e.inspect }
366
+ Dalli.logger.debug { "unable to get keys for server #{server.name}" }
310
367
  end
311
368
  end
312
369
 
@@ -324,44 +381,55 @@ module Dalli
324
381
  servers
325
382
  end
326
383
 
384
+ ##
385
+ # Ensures that the servers arg is either an array or a string.
386
+ def validate_servers_arg(servers)
387
+ return if servers.nil?
388
+ return if servers.is_a?(Array)
389
+ return if servers.is_a?(String)
390
+
391
+ raise ArgumentError, "An explicit servers argument must be a comma separated string or an array containing strings."
392
+ end
393
+
327
394
  ##
328
395
  # Normalizes the argument into an array of servers.
329
- # If the argument is a string, it's expected that the URIs are comma separated e.g.
396
+ # If the argument is a string, or an array containing strings, it's expected that the URIs are comma separated e.g.
330
397
  # "memcache1.example.com:11211,memcache2.example.com:11211,memcache3.example.com:11211"
331
398
  def normalize_servers(servers)
332
- if servers.is_a? String
333
- return servers.split(",")
334
- else
335
- return servers
399
+ Array(servers).flat_map do |server|
400
+ if server.is_a? String
401
+ server.split(",")
402
+ else
403
+ server
404
+ end
336
405
  end
337
406
  end
338
407
 
339
408
  def ring
340
409
  @ring ||= Dalli::Ring.new(
341
- @servers.map do |s|
342
- server_options = {}
343
- if s =~ %r{\Amemcached://}
410
+ @servers.map { |s|
411
+ server_options = {}
412
+ if s.start_with?("memcached://")
344
413
  uri = URI.parse(s)
345
414
  server_options[:username] = uri.user
346
415
  server_options[:password] = uri.password
347
416
  s = "#{uri.host}:#{uri.port}"
348
417
  end
349
- Dalli::Server.new(s, @options.merge(server_options))
350
- end, @options
418
+ @options.fetch(:protocol_implementation, Dalli::Protocol::Binary).new(s, @options.merge(server_options))
419
+ }, @options
351
420
  )
352
421
  end
353
422
 
354
423
  # Chokepoint method for instrumentation
355
424
  def perform(*all_args)
356
425
  return yield if block_given?
357
- op, key, *args = *all_args
426
+ op, key, *args = all_args
358
427
 
359
428
  key = key.to_s
360
429
  key = validate_key(key)
361
430
  begin
362
431
  server = ring.server_for_key(key)
363
- ret = server.request(op, key, *args)
364
- ret
432
+ server.request(op, key, *args)
365
433
  rescue NetworkError => e
366
434
  Dalli.logger.debug { e.inspect }
367
435
  Dalli.logger.debug { "retrying request with new server" }
@@ -373,10 +441,11 @@ module Dalli
373
441
  raise ArgumentError, "key cannot be blank" if !key || key.length == 0
374
442
  key = key_with_namespace(key)
375
443
  if key.length > 250
376
- max_length_before_namespace = 212 - (namespace || '').size
377
- key = "#{key[0, max_length_before_namespace]}:md5:#{Digest::MD5.hexdigest(key)}"
444
+ digest_class = @options[:digest_class] || ::Digest::MD5
445
+ max_length_before_namespace = 212 - (namespace || "").size
446
+ key = "#{key[0, max_length_before_namespace]}:md5:#{digest_class.hexdigest(key)}"
378
447
  end
379
- return key
448
+ key
380
449
  end
381
450
 
382
451
  def key_with_namespace(key)
@@ -384,7 +453,7 @@ module Dalli
384
453
  end
385
454
 
386
455
  def key_without_namespace(key)
387
- (ns = namespace) ? key.sub(%r(\A#{Regexp.escape ns}:), '') : key
456
+ (ns = namespace) ? key.sub(%r{\A#{Regexp.escape ns}:}, "") : key
388
457
  end
389
458
 
390
459
  def namespace
@@ -402,6 +471,9 @@ module Dalli
402
471
  rescue NoMethodError
403
472
  raise ArgumentError, "cannot convert :expires_in => #{opts[:expires_in].inspect} to an integer"
404
473
  end
474
+ if opts[:digest_class] && !opts[:digest_class].respond_to?(:hexdigest)
475
+ raise ArgumentError, "The digest_class object must respond to the hexdigest method"
476
+ end
405
477
  opts
406
478
  end
407
479
 
@@ -411,54 +483,52 @@ module Dalli
411
483
  perform do
412
484
  return {} if keys.empty?
413
485
  ring.lock do
414
- begin
415
- groups = groups_for_keys(keys)
416
- if unfound_keys = groups.delete(nil)
417
- Dalli.logger.debug { "unable to get keys for #{unfound_keys.length} keys because no matching server was found" }
418
- end
419
- make_multi_get_requests(groups)
420
-
421
- servers = groups.keys
422
- return if servers.empty?
423
- servers = perform_multi_response_start(servers)
424
-
425
- start = Time.now
426
- while true
427
- # remove any dead servers
428
- servers.delete_if { |s| s.sock.nil? }
429
- break if servers.empty?
430
-
431
- # calculate remaining timeout
432
- elapsed = Time.now - start
433
- timeout = servers.first.options[:socket_timeout]
434
- time_left = (elapsed > timeout) ? 0 : timeout - elapsed
435
-
436
- sockets = servers.map(&:sock)
437
- readable, _ = IO.select(sockets, nil, nil, time_left)
438
-
439
- if readable.nil?
440
- # no response within timeout; abort pending connections
441
- servers.each do |server|
442
- Dalli.logger.debug { "memcached at #{server.name} did not response within timeout" }
443
- server.multi_response_abort
444
- end
445
- break
486
+ groups = groups_for_keys(keys)
487
+ if (unfound_keys = groups.delete(nil))
488
+ Dalli.logger.debug { "unable to get keys for #{unfound_keys.length} keys because no matching server was found" }
489
+ end
490
+ make_multi_get_requests(groups)
491
+
492
+ servers = groups.keys
493
+ return if servers.empty?
494
+ servers = perform_multi_response_start(servers)
495
+
496
+ start = Time.now
497
+ loop do
498
+ # remove any dead servers
499
+ servers.delete_if { |s| s.sock.nil? }
500
+ break if servers.empty?
501
+
502
+ # calculate remaining timeout
503
+ elapsed = Time.now - start
504
+ timeout = servers.first.options[:socket_timeout]
505
+ time_left = elapsed > timeout ? 0 : timeout - elapsed
506
+
507
+ sockets = servers.map(&:sock)
508
+ readable, _ = IO.select(sockets, nil, nil, time_left)
509
+
510
+ if readable.nil?
511
+ # no response within timeout; abort pending connections
512
+ servers.each do |server|
513
+ Dalli.logger.debug { "memcached at #{server.name} did not response within timeout" }
514
+ server.multi_response_abort
515
+ end
516
+ break
446
517
 
447
- else
448
- readable.each do |sock|
449
- server = sock.server
518
+ else
519
+ readable.each do |sock|
520
+ server = sock.server
450
521
 
451
- begin
452
- server.multi_response_nonblock.each_pair do |key, value_list|
453
- yield key_without_namespace(key), value_list
454
- end
522
+ begin
523
+ server.multi_response_nonblock.each_pair do |key, value_list|
524
+ yield key_without_namespace(key), value_list
525
+ end
455
526
 
456
- if server.multi_response_completed?
457
- servers.delete(server)
458
- end
459
- rescue NetworkError
527
+ if server.multi_response_completed?
460
528
  servers.delete(server)
461
529
  end
530
+ rescue NetworkError
531
+ servers.delete(server)
462
532
  end
463
533
  end
464
534
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
- require 'zlib'
3
- require 'stringio'
2
+
3
+ require "zlib"
4
+ require "stringio"
4
5
 
5
6
  module Dalli
6
7
  class Compressor
@@ -15,7 +16,7 @@ module Dalli
15
16
 
16
17
  class GzipCompressor
17
18
  def self.compress(data)
18
- io = StringIO.new(String.new(""), "w")
19
+ io = StringIO.new(+"", "w")
19
20
  gz = Zlib::GzipWriter.new(io)
20
21
  gz.write(data)
21
22
  gz.close
data/lib/dalli/options.rb CHANGED
@@ -1,13 +1,12 @@
1
1
  # frozen_string_literal: true
2
- require 'thread'
3
- require 'monitor'
4
2
 
5
- module Dalli
3
+ require "monitor"
6
4
 
5
+ module Dalli
7
6
  # Make Dalli threadsafe by using a lock around all
8
7
  # public server methods.
9
8
  #
10
- # Dalli::Server.extend(Dalli::Threadsafe)
9
+ # Dalli::Protocol::Binary.extend(Dalli::Threadsafe)
11
10
  #
12
11
  module Threadsafe
13
12
  def self.extended(obj)